cleanup & fix ci (#82)

* cleanup readme & examples
* upgrade go version
* add makefile with basic commands
* add e2e test
* add gha
* update .gitignore
This commit is contained in:
Fran
2023-09-29 17:47:28 +02:00
committed by GitHub
parent 029b93e5f0
commit ec40467798
27 changed files with 688 additions and 943 deletions

View File

@@ -1,17 +0,0 @@
version: 2
jobs:
build:
docker:
# specify the version
- image: circleci/golang:1.14-buster
environment:
GO111MODULE: "on"
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
steps:
- checkout
# specify any bash command here prefixed with `run: `
- run: sudo apt-get update && sudo apt-get install ffmpeg
- run: go mod download
- run: go test -failfast -v -run=. ./...

25
.github/workflows/build_and_test.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Go
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21.x"
- name: Install dependencies
run: go get .
- name: Install FFmpeg
run: sudo apt-get update && sudo apt-get install ffmpeg
- name: Build
run: go build -v ./...
- name: Test with the Go CLI
run: go test --failfast -v ./...

6
.gitignore vendored
View File

@@ -20,6 +20,10 @@ main-test.go
goffmpeg
coverage.out
# IDE
.DS_Store
.vscode
.idea
.idea
# test results
test_results

7
Makefile Normal file
View File

@@ -0,0 +1,7 @@
test:
go test -v ./... -coverprofile=coverage.out -covermode=atomic -coverpkg=./...
coverage-html:
go tool cover -html=coverage.out

278
README.md
View File

@@ -1,273 +1,41 @@
# Goffmpeg
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/93e018e5008b4439acbb30d715b22e7f)](https://www.codacy.com/app/francisco.romero/goffmpeg?utm_source=github.com&utm_medium=referral&utm_content=xfrr/goffmpeg&utm_campaign=Badge_Grade)
[![CircleCI](https://dl.circleci.com/status-badge/img/gh/xfrr/goffmpeg/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/xfrr/goffmpeg/tree/master)
[![Go Report Card](https://goreportcard.com/badge/github.com/xfrr/goffmpeg)](https://goreportcard.com/report/github.com/xfrr/goffmpeg)
[![GoDoc](https://godoc.org/github.com/xfrr/goffmpeg?status.svg)](https://godoc.org/github.com/xfrr/goffmpeg)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](
FFMPEG wrapper written in GO which allows to obtain the progress.
FFMPEG wrapper written in GO
## V2
New implementation with an easy-to-use API and interfaces to extend the transcoding capabilities.
> New implementation with an easy-to-use API and interfaces to extend the transcoding capabilities.
> https://github.com/floostack/transcoder
# Dependencies
## Features
- [x] Transcoding
- [x] Streaming
- [x] Progress
- [x] Filters
- [x] Thumbnails
- [x] Watermark
- [ ] Concatenation
- [ ] Subtitles
## Dependencies
- [FFmpeg](https://www.ffmpeg.org/)
- [FFProbe](https://www.ffmpeg.org/ffprobe.html)
# Supported platforms
## Supported platforms
- Linux
- OS X
- Windows
# Getting started
## How to transcode a media file
## Installation
Install the package with the following command:
```shell
go get github.com/xfrr/goffmpeg
```
```go
package main
import (
"github.com/xfrr/goffmpeg/transcoder"
)
var inputPath = "/data/testmov"
var outputPath = "/data/testmp4.mp4"
func main() {
// Create new instance of transcoder
trans := new(transcoder.Transcoder)
// Initialize transcoder passing the input file path and output file path
err := trans.Initialize( inputPath, outputPath )
// Handle error...
// Start transcoder process without checking progress
done := trans.Run(false)
// This channel is used to wait for the process to end
err = <-done
// Handle error...
}
```
## How to get the transcoding progress
```go
...
func main() {
// Create new instance of transcoder
trans := new(transcoder.Transcoder)
// Initialize transcoder passing the input file path and output file path
err := trans.Initialize( inputPath, outputPath )
// Handle error...
// Start transcoder process with progress checking
done := trans.Run(true)
// Returns a channel to get the transcoding progress
progress := trans.Output()
// Example of printing transcoding progress
for msg := range progress {
fmt.Println(msg)
}
// This channel is used to wait for the transcoding process to end
err = <-done
}
```
## How to pipe in data using the [pipe protocol](https://ffmpeg.org/ffmpeg-protocols.html#pipe)
Creating an input pipe will return [\*io.PipeReader](https://golang.org/pkg/io/#PipeReader), and creating an output pipe will return [\*io.PipeWriter](https://golang.org/pkg/io/#PipeWriter). An example is shown which uses `cat` to pipe in data, and [ioutil.ReadAll](https://golang.org/pkg/io/ioutil/#ReadAll) to read data as bytes from the pipe.
```go
func main() {
// Create new instance of transcoder
trans := new(transcoder.Transcoder)
// Initialize an empty transcoder
err := trans.InitializeEmptyTranscoder()
// Handle error...
// Create a command such that its output should be passed as stdin to ffmpeg
cmd := exec.Command("cat", "/path/to/file")
// Create an input pipe to write to, which will return *io.PipeWriter
w, err := trans.CreateInputPipe()
cmd.Stdout = w
// Create an output pipe to read from, which will return *io.PipeReader.
// Must also specify the output container format
r, err := trans.CreateOutputPipe("mp4")
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer r.Close()
defer wg.Done()
// Read data from output pipe
data, err := ioutil.ReadAll(r)
// Handle error and data...
}()
go func() {
defer w.Close()
err := cmd.Run()
// Handle error...
}()
// Start transcoder process without checking progress
done := trans.Run(false)
// This channel is used to wait for the transcoding process to end
err = <-done
// Handle error...
wg.Wait()
}
```
# Progress properties
```go
type Progress struct {
FramesProcessed string
CurrentTime string
CurrentBitrate string
Progress float64
Speed string
}
```
# Media setters
Those options can be set before starting the transcoding.
```js
SetAspect
SetResolution
SetVideoBitRate
SetVideoBitRateTolerance
SetVideoMaxBitrate
SetVideoMinBitRate
SetVideoCodec
SetVframes
SetFrameRate
SetAudioRate
SetSkipAudio
SetSkipVideo
SetMaxKeyFrame
SetMinKeyFrame
SetKeyframeInterval
SetAudioCodec
SetAudioBitRate
SetAudioChannels
SetBufferSize
SetThreads
SetPreset
SetTune
SetAudioProfile
SetVideoProfile
SetDuration
SetDurationInput
SetSeekTime
SetSeekTimeInput
SetSeekUsingTsInput
SetQuality
SetStrict
SetSingleFile
SetCopyTs
SetMuxDelay
SetHideBanner
SetInputPath
SetNativeFramerateInput
SetRtmpLive
SetHlsListSize
SetHlsSegmentDuration
SetHlsPlaylistType
SetHlsMasterPlaylistName
SetHlsSegmentFilename
SetHttpMethod
SetHttpKeepAlive
SetOutputPath
SetOutputFormat
SetAudioFilter
SetAudioVariableBitrate
SetCompressionLevel
SetFilter
SetInputInitialOffset
SetInputPipeCommand
SetMapMetadata
SetMetadata
SetStreamIds
SetTags
SetVideoFilter
```
Example
```golang
func main() {
// Create new instance of transcoder
trans := new(transcoder.Transcoder)
// Initialize transcoder passing the input file path and output file path
err := trans.Initialize( inputPath, outputPath )
// Handle error...
// SET FRAME RATE TO MEDIAFILE
trans.MediaFile().SetFrameRate(70)
// SET ULTRAFAST PRESET TO MEDIAFILE
trans.MediaFile().SetPreset("ultrafast")
// Start transcoder process to check progress
done := trans.Run(true)
// Returns a channel to get the transcoding progress
progress := trans.Output()
// Example of printing transcoding progress
for msg := range progress {
fmt.Println(msg)
}
// This channel is used to wait for the transcoding process to end
err = <-done
}
```
Example with AES encryption :
More information about [HLS encryption with FFMPEG](https://hlsbook.net/how-to-encrypt-hls-video-with-ffmpeg/)
```bash
# Generate key
openssl rand 16 > enc.key
```
Create key file info :
```enc.keyinfo
Key URI
Path to key file
```
```golang
func main() {
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
trans.MediaFile().SetVideoCodec("libx264")
trans.MediaFile().SetHlsSegmentDuration(4)
trans.MediaFile().SetEncryptionKey(keyinfoPath)
progress := trans.Output()
err = <-done
}
```
## Usage
Check the [examples](./examples)

67
config.go Normal file
View File

@@ -0,0 +1,67 @@
package goffmpeg
import (
"context"
"errors"
"runtime"
"strings"
"github.com/xfrr/goffmpeg/pkg/cmd"
)
const (
ffmpegCommand = "ffmpeg"
ffprobeCommand = "ffprobe"
)
type Configuration struct {
ffprobeBinPath string
ffmpegBinPath string
}
func (cfg Configuration) FFmpegBinPath() string {
return cfg.ffmpegBinPath
}
func (cfg Configuration) FFprobeBinPath() string {
return cfg.ffprobeBinPath
}
func Configure(ctx context.Context) (Configuration, error) {
ffmpegBin, err := cmd.FindBinPath(ctx, ffmpegCommand)
if err != nil {
return Configuration{}, err
}
if ffmpegBin == "" {
return Configuration{}, errors.New("ffmpeg not found, please install it before using goffmpeg")
}
ffprobeBin, err := cmd.FindBinPath(ctx, ffprobeCommand)
if err != nil {
return Configuration{}, err
}
if ffprobeBin == "" {
return Configuration{}, errors.New("ffprobe not found, please install it before using goffmpeg")
}
return Configuration{
ffmpegBinPath: normalizeBinPath(ffmpegBin),
ffprobeBinPath: normalizeBinPath(ffprobeBin),
}, nil
}
func normalizeBinPath(binPath string) string {
binPath = strings.ReplaceAll(binPath, lineSeparator(), " ")
return strings.TrimSpace(binPath)
}
func lineSeparator() string {
switch runtime.GOOS {
case "windows":
return "\r\n"
default:
return "\n"
}
}

18
config_test.go Normal file
View File

@@ -0,0 +1,18 @@
package goffmpeg
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigure(t *testing.T) {
t.Run("Should set the correct command", func(t *testing.T) {
ctx := context.Background()
cfg, err := Configure(ctx)
assert.Nil(t, err)
assert.NotEmpty(t, cfg.FFmpegBinPath())
assert.NotEmpty(t, cfg.FFprobeBinPath())
})
}

BIN
e2e/fixtures/input.3gp Normal file

Binary file not shown.

104
e2e/transcoding_test.go Executable file
View File

@@ -0,0 +1,104 @@
package test
import (
"io/ioutil"
"os/exec"
"path"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/xfrr/goffmpeg/transcoder"
)
const (
fixturePath = "./fixtures"
resultsPath = "./test_results"
)
var (
// Input files
input3gp = path.Join(fixturePath, "input.3gp")
)
func TestInputNotFound(t *testing.T) {
createResultsDir(t)
var outputPath = path.Join(resultsPath, "notfound.mp4")
trans := new(transcoder.Transcoder)
err := trans.Initialize("notfound.3gp", outputPath)
assert.NotNil(t, err)
}
func TestTranscodingProgress(t *testing.T) {
createResultsDir(t)
outputPath := path.Join(resultsPath, "progress.mp4")
trans := new(transcoder.Transcoder)
err := trans.Initialize(input3gp, outputPath)
assert.Nil(t, err)
errCh := trans.Run(true)
progress := []transcoder.Progress{}
for val := range trans.Output() {
progress = append(progress, val)
}
err = <-errCh
assert.Nil(t, err)
assert.GreaterOrEqual(t, len(progress), 1)
checkFileExists(t, outputPath)
}
func TestTranscodePipes(t *testing.T) {
createResultsDir(t)
c1 := exec.Command("cat", input3gp)
trans := new(transcoder.Transcoder)
err := trans.InitializeEmptyTranscoder()
assert.Nil(t, err)
w, err := trans.CreateInputPipe()
assert.Nil(t, err)
c1.Stdout = w
r, err := trans.CreateOutputPipe("mp4")
assert.Nil(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
_, err := ioutil.ReadAll(r)
assert.Nil(t, err)
r.Close()
wg.Done()
}()
go func() {
err := c1.Run()
assert.Nil(t, err)
w.Close()
}()
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
wg.Wait()
}
func createResultsDir(t *testing.T) {
err := exec.Command("mkdir", "-p", resultsPath).Run()
assert.Nil(t, err)
}
func checkFileExists(t *testing.T, filepath string) {
res, err := exec.Command("cat", filepath).Output()
assert.Nil(t, err)
assert.Greater(t, len(res), 0)
}

BIN
examples/fixtures/input.3gp Normal file

Binary file not shown.

34
examples/hls/main.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"fmt"
"github.com/xfrr/goffmpeg/transcoder"
)
const (
inputPath = "../fixtures/input.3gp"
outputPath = "../test_results/hls-output.mp4"
keyinfoPath = "keyinfo"
)
func main() {
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
if err != nil {
panic(err)
}
trans.MediaFile().SetVideoCodec("libx264")
trans.MediaFile().SetHlsSegmentDuration(4)
trans.MediaFile().SetEncryptionKey(keyinfoPath)
done := trans.Run(true)
progress := trans.Output()
for p := range progress {
fmt.Println(p)
}
fmt.Println(<-done)
}

View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"github.com/xfrr/goffmpeg/transcoder"
)
const (
inputPath = "../fixtures/input.3gp"
outputPath = "../test_results/ultrafast-output.mp4"
)
func main() {
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
if err != nil {
panic(err)
}
trans.MediaFile().SetPreset("ultrafast")
done := trans.Run(true)
progress := trans.Output()
for p := range progress {
fmt.Println(p)
}
fmt.Println(<-done)
}

View File

@@ -1,39 +0,0 @@
package ffmpeg
import (
"bytes"
"strings"
"github.com/xfrr/goffmpeg/utils"
)
// Configuration ...
type Configuration struct {
FfmpegBin string
FfprobeBin string
}
// Configure Get and set FFmpeg and FFprobe bin paths
func Configure() (Configuration, error) {
var outFFmpeg bytes.Buffer
var outProbe bytes.Buffer
execFFmpegCommand := utils.GetFFmpegExec()
execFFprobeCommand := utils.GetFFprobeExec()
outFFmpeg, err := utils.TestCmd(execFFmpegCommand[0], execFFmpegCommand[1])
if err != nil {
return Configuration{}, err
}
outProbe, err = utils.TestCmd(execFFprobeCommand[0], execFFprobeCommand[1])
if err != nil {
return Configuration{}, err
}
ffmpeg := strings.Replace(strings.Split(outFFmpeg.String(), "\n")[0], utils.LineSeparator(), "", -1)
ffprobe := strings.Replace(strings.Split(outProbe.String(), "\n")[0], utils.LineSeparator(), "", -1)
cnf := Configuration{ffmpeg, ffprobe}
return cnf, nil
}

8
go.mod
View File

@@ -1,5 +1,11 @@
module github.com/xfrr/goffmpeg
go 1.14
go 1.20
require github.com/stretchr/testify v1.5.1
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)

File diff suppressed because it is too large Load Diff

18
media/format.go Normal file
View File

@@ -0,0 +1,18 @@
package media
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 Tags struct {
Encoder string `json:"ENCODER"`
}

6
media/metadata.go Normal file
View File

@@ -0,0 +1,6 @@
package media
type Metadata struct {
Streams []Streams `json:"streams"`
Format Format `json:"format"`
}

View File

@@ -1,14 +1,4 @@
package models
type Ffmpeg struct {
FfmpegBinPath string
FfprobeBinPath string
}
type Metadata struct {
Streams []Streams `json:"streams"`
Format Format `json:"format"`
}
package media
type Streams struct {
Index int
@@ -54,28 +44,3 @@ type Disposition struct {
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
Speed string
}
type Tags struct {
Encoder string `json:"ENCODER"`
}

View File

@@ -1,21 +0,0 @@
package models
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestMedia(t *testing.T) {
t.Run("#ObtainEncryptionKey", func(t *testing.T) {
t.Run("Should get nil if encryptionKey is not set", func(t *testing.T) {
mediaFile := Mediafile{}
require.Nil(t, mediaFile.ObtainEncryptionKey())
})
t.Run("Should return file.keyinfo if it's set", func(t *testing.T) {
mediaFile := Mediafile{encryptionKey: "file.keyinfo"}
require.Equal(t, []string{"-hls_key_info_file", "file.keyinfo"}, mediaFile.ObtainEncryptionKey())
})
})
}

35
pkg/cmd/exec.go Normal file
View File

@@ -0,0 +1,35 @@
package cmd
import (
"bytes"
"context"
"fmt"
"os/exec"
)
func FindBinPath(ctx context.Context, command string) (string, error) {
if command == "" {
return "", fmt.Errorf("command cannot be empty")
}
path, err := execBufferOutput(ctx, getFindCommand(), command)
if err != nil {
return "", err
}
return path, nil
}
func execBufferOutput(ctx context.Context, command string, args ...string) (string, error) {
var out bytes.Buffer
c := exec.CommandContext(ctx, command, args...)
c.Stdout = &out
err := c.Run()
if err != nil {
return "", fmt.Errorf("%s: %s", c.String(), err)
}
return out.String(), nil
}

16
pkg/cmd/find.go Normal file
View File

@@ -0,0 +1,16 @@
package cmd
import "runtime"
var (
platform = runtime.GOOS
)
func getFindCommand() string {
switch platform {
case "windows":
return "where"
default:
return "which"
}
}

21
pkg/duration/duration.go Normal file
View File

@@ -0,0 +1,21 @@
package duration
import (
"strconv"
"strings"
)
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
}

View File

@@ -1,231 +0,0 @@
package test
import (
"io/ioutil"
"os/exec"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/xfrr/goffmpeg/transcoder"
)
func TestInputNotFound(t *testing.T) {
var inputPath = "/tmp/ffmpeg/nf"
var outputPath = "/tmp/ffmpeg/out/nf.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.NotNil(t, err)
}
func TestTranscoding3GP(t *testing.T) {
var inputPath = "/tmp/ffmpeg/3gp"
var outputPath = "/tmp/ffmpeg/out/3gp.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingAVI(t *testing.T) {
var inputPath = "/tmp/ffmpeg/avi"
var outputPath = "/tmp/ffmpeg/out/avi.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingFLV(t *testing.T) {
var inputPath = "/tmp/ffmpeg/flv"
var outputPath = "/tmp/ffmpeg/out/flv.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingMKV(t *testing.T) {
var inputPath = "/tmp/ffmpeg/mkv"
var outputPath = "/tmp/ffmpeg/out/mkv.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingMOV(t *testing.T) {
var inputPath = "/tmp/ffmpeg/mov"
var outputPath = "/tmp/ffmpeg/out/mov.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingMPEG(t *testing.T) {
var inputPath = "/tmp/ffmpeg/mpeg"
var outputPath = "/tmp/ffmpeg/out/mpeg.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingOGG(t *testing.T) {
var inputPath = "/tmp/ffmpeg/ogg"
var outputPath = "/tmp/ffmpeg/out/ogg.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingWAV(t *testing.T) {
var inputPath = "/tmp/ffmpeg/wav"
var outputPath = "/tmp/ffmpeg/out/wav.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingWEBM(t *testing.T) {
var inputPath = "/tmp/ffmpeg/webm"
var outputPath = "/tmp/ffmpeg/out/webm.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingWMV(t *testing.T) {
var inputPath = "/tmp/ffmpeg/wmv"
var outputPath = "/tmp/ffmpeg/out/wmv.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
}
func TestTranscodingProgress(t *testing.T) {
var inputPath = "/tmp/ffmpeg/avi"
var outputPath = "/tmp/ffmpeg/out/avi.mp4"
trans := new(transcoder.Transcoder)
err := trans.Initialize(inputPath, outputPath)
assert.Nil(t, err)
done := trans.Run(true)
for val := range trans.Output() {
if &val != nil {
break
}
}
err = <-done
assert.Nil(t, err)
}
func TestTranscodePipes(t *testing.T) {
c1 := exec.Command("cat", "/tmp/ffmpeg/mkv")
trans := new(transcoder.Transcoder)
err := trans.InitializeEmptyTranscoder()
assert.Nil(t, err)
w, err := trans.CreateInputPipe()
assert.Nil(t, err)
c1.Stdout = w
r, err := trans.CreateOutputPipe("mp4")
assert.Nil(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
_, err := ioutil.ReadAll(r)
assert.Nil(t, err)
r.Close()
wg.Done()
}()
go func() {
err := c1.Run()
assert.Nil(t, err)
w.Close()
}()
done := trans.Run(false)
err = <-done
assert.Nil(t, err)
wg.Wait()
}

9
transcoder/progress.go Normal file
View File

@@ -0,0 +1,9 @@
package transcoder
type Progress struct {
FramesProcessed string
CurrentTime string
CurrentBitrate string
Progress float64
Speed string
}

View File

@@ -3,6 +3,7 @@ package transcoder
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -12,18 +13,18 @@ import (
"strconv"
"strings"
"github.com/xfrr/goffmpeg/ffmpeg"
"github.com/xfrr/goffmpeg/models"
"github.com/xfrr/goffmpeg/utils"
"github.com/xfrr/goffmpeg"
"github.com/xfrr/goffmpeg/media"
"github.com/xfrr/goffmpeg/pkg/duration"
)
// Transcoder Main struct
type Transcoder struct {
stdErrPipe io.ReadCloser
stdStdinPipe io.WriteCloser
process *exec.Cmd
mediafile *models.Mediafile
configuration ffmpeg.Configuration
stdErrPipe io.ReadCloser
stdStdinPipe io.WriteCloser
process *exec.Cmd
mediafile *media.File
configuration goffmpeg.Configuration
whiteListProtocols []string
}
@@ -43,12 +44,12 @@ func (t *Transcoder) SetProcess(cmd *exec.Cmd) {
}
// SetMediaFile Set the media file
func (t *Transcoder) SetMediaFile(v *models.Mediafile) {
func (t *Transcoder) SetMediaFile(v *media.File) {
t.mediafile = v
}
// SetConfiguration Set the transcoding configuration
func (t *Transcoder) SetConfiguration(v ffmpeg.Configuration) {
func (t *Transcoder) SetConfiguration(v goffmpeg.Configuration) {
t.configuration = v
}
@@ -62,18 +63,18 @@ func (t Transcoder) Process() *exec.Cmd {
}
// MediaFile Get the ttranscoding media file.
func (t Transcoder) MediaFile() *models.Mediafile {
func (t Transcoder) MediaFile() *media.File {
return t.mediafile
}
// FFmpegExec Get FFmpeg Bin path
func (t Transcoder) FFmpegExec() string {
return t.configuration.FfmpegBin
return t.configuration.FFmpegBinPath()
}
// FFprobeExec Get FFprobe Bin path
func (t Transcoder) FFprobeExec() string {
return t.configuration.FfprobeBin
return t.configuration.FFprobeBinPath()
}
// GetCommand Build and get command
@@ -90,18 +91,18 @@ func (t Transcoder) GetCommand() []string {
// InitializeEmptyTranscoder initializes the fields necessary for a blank transcoder
func (t *Transcoder) InitializeEmptyTranscoder() error {
var Metadata models.Metadata
var Metadata media.Metadata
var err error
cfg := t.configuration
if len(cfg.FfmpegBin) == 0 || len(cfg.FfprobeBin) == 0 {
cfg, err = ffmpeg.Configure()
if len(cfg.FFmpegBinPath()) == 0 || len(cfg.FFprobeBinPath()) == 0 {
cfg, err = goffmpeg.Configure(context.Background())
if err != nil {
return err
}
}
// Set new Mediafile
MediaFile := new(models.Mediafile)
// Set new File
MediaFile := new(media.File)
MediaFile.SetMetadata(Metadata)
// Set transcoder configuration
@@ -159,12 +160,12 @@ func (t *Transcoder) CreateOutputPipe(containerFormat string) (*io.PipeReader, e
func (t *Transcoder) Initialize(inputPath string, outputPath string) error {
var err error
var outb, errb bytes.Buffer
var Metadata models.Metadata
var Metadata media.Metadata
cfg := t.configuration
if len(cfg.FfmpegBin) == 0 || len(cfg.FfprobeBin) == 0 {
cfg, err = ffmpeg.Configure()
if len(cfg.FFmpegBinPath()) == 0 || len(cfg.FFprobeBinPath()) == 0 {
cfg, err = goffmpeg.Configure(context.Background())
if err != nil {
return err
}
@@ -180,7 +181,7 @@ func (t *Transcoder) Initialize(inputPath string, outputPath string) error {
command = append([]string{"-protocol_whitelist", strings.Join(t.whiteListProtocols, ",")}, command...)
}
cmd := exec.Command(cfg.FfprobeBin, command...)
cmd := exec.Command(cfg.FFprobeBinPath(), command...)
cmd.Stdout = &outb
cmd.Stderr = &errb
@@ -189,12 +190,12 @@ func (t *Transcoder) Initialize(inputPath string, outputPath string) error {
return fmt.Errorf("error executing (%s) | error: %s | message: %s %s", command, err, outb.String(), errb.String())
}
if err = json.Unmarshal([]byte(outb.String()), &Metadata); err != nil {
if err = json.Unmarshal(outb.Bytes(), &Metadata); err != nil {
return err
}
// Set new Mediafile
MediaFile := new(models.Mediafile)
// Set new File
MediaFile := new(media.File)
MediaFile.SetMetadata(Metadata)
MediaFile.SetInputPath(inputPath)
MediaFile.SetOutputPath(outputPath)
@@ -216,7 +217,7 @@ func (t *Transcoder) Run(progress bool) <-chan error {
command = append([]string{"-nostats", "-loglevel", "0"}, command...)
}
proc := exec.Command(t.configuration.FfmpegBin, command...)
proc := exec.Command(t.configuration.FFmpegBinPath(), command...)
if progress {
errStream, err := proc.StderrPipe()
if err != nil {
@@ -256,7 +257,7 @@ func (t *Transcoder) Run(progress bool) <-chan error {
go func(err error) {
if err != nil {
done <- fmt.Errorf("Failed Start FFMPEG (%s) with %s, message %s %s", command, err, outb.String(), errb.String())
done <- fmt.Errorf("failed start ffmpeg (%s) with %s, message %s %s", command, err, outb.String(), errb.String())
close(done)
return
}
@@ -266,7 +267,7 @@ func (t *Transcoder) Run(progress bool) <-chan error {
go t.closePipes()
if err != nil {
err = fmt.Errorf("Failed Finish FFMPEG (%s) with %s message %s %s", command, err, outb.String(), errb.String())
err = fmt.Errorf("failed finish ffmpeg (%s) with %s message %s %s", command, err, outb.String(), errb.String())
}
done <- err
close(done)
@@ -288,13 +289,13 @@ func (t *Transcoder) Stop() error {
}
// Output Returns the transcoding progress channel
func (t Transcoder) Output() <-chan models.Progress {
out := make(chan models.Progress)
func (t Transcoder) Output() <-chan Progress {
out := make(chan Progress)
go func() {
defer close(out)
if t.stdErrPipe == nil {
out <- models.Progress{}
out <- Progress{}
return
}
@@ -328,7 +329,7 @@ func (t Transcoder) Output() <-chan models.Progress {
scanner.Buffer(buf, bufio.MaxScanTokenSize)
for scanner.Scan() {
Progress := new(models.Progress)
Progress := new(Progress)
line := scanner.Text()
if strings.Contains(line, "frame=") && strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") {
var re = regexp.MustCompile(`=\s+`)
@@ -365,7 +366,7 @@ func (t Transcoder) Output() <-chan models.Progress {
}
}
timesec := utils.DurToSec(currentTime)
timesec := duration.DurToSec(currentTime)
dursec, _ := strconv.ParseFloat(t.MediaFile().Metadata().Format.Duration, 64)
//live stream check
if dursec != 0 {

View File

@@ -1,9 +1,10 @@
package transcoder
import (
"github.com/stretchr/testify/require"
"github.com/xfrr/goffmpeg/models"
"testing"
"github.com/stretchr/testify/require"
"github.com/xfrr/goffmpeg/media"
)
func TestTranscoder(t *testing.T) {
@@ -11,7 +12,7 @@ func TestTranscoder(t *testing.T) {
t.Run("Should not set -protocol_whitelist option if it isn't present", func(t *testing.T) {
ts := Transcoder{}
ts.SetMediaFile(&models.Mediafile{})
ts.SetMediaFile(&media.File{})
require.NotEqual(t, ts.GetCommand()[0:2], []string{"-protocol_whitelist", "file,http,https,tcp,tls"})
require.NotContains(t, ts.GetCommand(), "protocol_whitelist")
})
@@ -19,8 +20,8 @@ func TestTranscoder(t *testing.T) {
t.Run("Should set -protocol_whitelist option if it's present", func(t *testing.T) {
ts := Transcoder{}
ts.SetMediaFile(&models.Mediafile{})
ts.SetWhiteListProtocols([]string{"file","http","https","tcp","tls"})
ts.SetMediaFile(&media.File{})
ts.SetWhiteListProtocols([]string{"file", "http", "https", "tcp", "tls"})
require.Equal(t, ts.GetCommand()[0:2], []string{"-protocol_whitelist", "file,http,https,tcp,tls"})
})

View File

@@ -1,94 +0,0 @@
package utils
import (
"bytes"
"fmt"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/xfrr/goffmpeg/models"
)
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
}
func GetFFmpegExec() []string {
var platform = runtime.GOOS
var command = []string{"", "ffmpeg"}
switch platform {
case "windows":
command[0] = "where"
break
default:
command[0] = "which"
break
}
return command
}
func GetFFprobeExec() []string {
var platform = runtime.GOOS
var command = []string{"", "ffprobe"}
switch platform {
case "windows":
command[0] = "where"
break
default:
command[0] = "which"
break
}
return command
}
func CheckFileType(streams []models.Streams) string {
for i := 0; i < len(streams); i++ {
st := streams[i]
if st.CodecType == "video" {
return "video"
}
}
return "audio"
}
func LineSeparator() string {
switch runtime.GOOS {
case "windows":
return "\r\n"
default:
return "\n"
}
}
// TestCmd ...
func TestCmd(command string, args string) (bytes.Buffer, error) {
var out bytes.Buffer
cmd := exec.Command(command, args)
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return out, fmt.Errorf("%s: %s", cmd.String(), err)
}
return out, nil
}