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 ./...

4
.gitignore vendored
View File

@@ -20,6 +20,10 @@ main-test.go
goffmpeg goffmpeg
coverage.out coverage.out
# IDE
.DS_Store .DS_Store
.vscode .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 # 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) [![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 > 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/) - [FFmpeg](https://www.ffmpeg.org/)
- [FFProbe](https://www.ffmpeg.org/ffprobe.html) - [FFProbe](https://www.ffmpeg.org/ffprobe.html)
# Supported platforms ## Supported platforms
- Linux - Linux
- OS X - OS X
- Windows - Windows
# Getting started ## Installation
## How to transcode a media file Install the package with the following command:
```shell ```shell
go get github.com/xfrr/goffmpeg go get github.com/xfrr/goffmpeg
``` ```
```go ## Usage
package main Check the [examples](./examples)
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
}
```

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 module github.com/xfrr/goffmpeg
go 1.14 go 1.20
require github.com/stretchr/testify v1.5.1 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 package media
type Ffmpeg struct {
FfmpegBinPath string
FfprobeBinPath string
}
type Metadata struct {
Streams []Streams `json:"streams"`
Format Format `json:"format"`
}
type Streams struct { type Streams struct {
Index int Index int
@@ -54,28 +44,3 @@ type Disposition struct {
VisualImpaired int `json:"visual_impaired"` VisualImpaired int `json:"visual_impaired"`
CleanEffects int `json:"clean_effects"` 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 ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -12,9 +13,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/xfrr/goffmpeg/ffmpeg" "github.com/xfrr/goffmpeg"
"github.com/xfrr/goffmpeg/models" "github.com/xfrr/goffmpeg/media"
"github.com/xfrr/goffmpeg/utils" "github.com/xfrr/goffmpeg/pkg/duration"
) )
// Transcoder Main struct // Transcoder Main struct
@@ -22,8 +23,8 @@ type Transcoder struct {
stdErrPipe io.ReadCloser stdErrPipe io.ReadCloser
stdStdinPipe io.WriteCloser stdStdinPipe io.WriteCloser
process *exec.Cmd process *exec.Cmd
mediafile *models.Mediafile mediafile *media.File
configuration ffmpeg.Configuration configuration goffmpeg.Configuration
whiteListProtocols []string whiteListProtocols []string
} }
@@ -43,12 +44,12 @@ func (t *Transcoder) SetProcess(cmd *exec.Cmd) {
} }
// SetMediaFile Set the media file // SetMediaFile Set the media file
func (t *Transcoder) SetMediaFile(v *models.Mediafile) { func (t *Transcoder) SetMediaFile(v *media.File) {
t.mediafile = v t.mediafile = v
} }
// SetConfiguration Set the transcoding configuration // SetConfiguration Set the transcoding configuration
func (t *Transcoder) SetConfiguration(v ffmpeg.Configuration) { func (t *Transcoder) SetConfiguration(v goffmpeg.Configuration) {
t.configuration = v t.configuration = v
} }
@@ -62,18 +63,18 @@ func (t Transcoder) Process() *exec.Cmd {
} }
// MediaFile Get the ttranscoding media file. // MediaFile Get the ttranscoding media file.
func (t Transcoder) MediaFile() *models.Mediafile { func (t Transcoder) MediaFile() *media.File {
return t.mediafile return t.mediafile
} }
// FFmpegExec Get FFmpeg Bin path // FFmpegExec Get FFmpeg Bin path
func (t Transcoder) FFmpegExec() string { func (t Transcoder) FFmpegExec() string {
return t.configuration.FfmpegBin return t.configuration.FFmpegBinPath()
} }
// FFprobeExec Get FFprobe Bin path // FFprobeExec Get FFprobe Bin path
func (t Transcoder) FFprobeExec() string { func (t Transcoder) FFprobeExec() string {
return t.configuration.FfprobeBin return t.configuration.FFprobeBinPath()
} }
// GetCommand Build and get command // GetCommand Build and get command
@@ -90,18 +91,18 @@ func (t Transcoder) GetCommand() []string {
// InitializeEmptyTranscoder initializes the fields necessary for a blank transcoder // InitializeEmptyTranscoder initializes the fields necessary for a blank transcoder
func (t *Transcoder) InitializeEmptyTranscoder() error { func (t *Transcoder) InitializeEmptyTranscoder() error {
var Metadata models.Metadata var Metadata media.Metadata
var err error var err error
cfg := t.configuration cfg := t.configuration
if len(cfg.FfmpegBin) == 0 || len(cfg.FfprobeBin) == 0 { if len(cfg.FFmpegBinPath()) == 0 || len(cfg.FFprobeBinPath()) == 0 {
cfg, err = ffmpeg.Configure() cfg, err = goffmpeg.Configure(context.Background())
if err != nil { if err != nil {
return err return err
} }
} }
// Set new Mediafile // Set new File
MediaFile := new(models.Mediafile) MediaFile := new(media.File)
MediaFile.SetMetadata(Metadata) MediaFile.SetMetadata(Metadata)
// Set transcoder configuration // 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 { func (t *Transcoder) Initialize(inputPath string, outputPath string) error {
var err error var err error
var outb, errb bytes.Buffer var outb, errb bytes.Buffer
var Metadata models.Metadata var Metadata media.Metadata
cfg := t.configuration cfg := t.configuration
if len(cfg.FfmpegBin) == 0 || len(cfg.FfprobeBin) == 0 { if len(cfg.FFmpegBinPath()) == 0 || len(cfg.FFprobeBinPath()) == 0 {
cfg, err = ffmpeg.Configure() cfg, err = goffmpeg.Configure(context.Background())
if err != nil { if err != nil {
return err 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...) 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.Stdout = &outb
cmd.Stderr = &errb 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()) 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 return err
} }
// Set new Mediafile // Set new File
MediaFile := new(models.Mediafile) MediaFile := new(media.File)
MediaFile.SetMetadata(Metadata) MediaFile.SetMetadata(Metadata)
MediaFile.SetInputPath(inputPath) MediaFile.SetInputPath(inputPath)
MediaFile.SetOutputPath(outputPath) MediaFile.SetOutputPath(outputPath)
@@ -216,7 +217,7 @@ func (t *Transcoder) Run(progress bool) <-chan error {
command = append([]string{"-nostats", "-loglevel", "0"}, command...) command = append([]string{"-nostats", "-loglevel", "0"}, command...)
} }
proc := exec.Command(t.configuration.FfmpegBin, command...) proc := exec.Command(t.configuration.FFmpegBinPath(), command...)
if progress { if progress {
errStream, err := proc.StderrPipe() errStream, err := proc.StderrPipe()
if err != nil { if err != nil {
@@ -256,7 +257,7 @@ func (t *Transcoder) Run(progress bool) <-chan error {
go func(err error) { go func(err error) {
if err != nil { 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) close(done)
return return
} }
@@ -266,7 +267,7 @@ func (t *Transcoder) Run(progress bool) <-chan error {
go t.closePipes() go t.closePipes()
if err != nil { 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 done <- err
close(done) close(done)
@@ -288,13 +289,13 @@ func (t *Transcoder) Stop() error {
} }
// Output Returns the transcoding progress channel // Output Returns the transcoding progress channel
func (t Transcoder) Output() <-chan models.Progress { func (t Transcoder) Output() <-chan Progress {
out := make(chan models.Progress) out := make(chan Progress)
go func() { go func() {
defer close(out) defer close(out)
if t.stdErrPipe == nil { if t.stdErrPipe == nil {
out <- models.Progress{} out <- Progress{}
return return
} }
@@ -328,7 +329,7 @@ func (t Transcoder) Output() <-chan models.Progress {
scanner.Buffer(buf, bufio.MaxScanTokenSize) scanner.Buffer(buf, bufio.MaxScanTokenSize)
for scanner.Scan() { for scanner.Scan() {
Progress := new(models.Progress) Progress := new(Progress)
line := scanner.Text() line := scanner.Text()
if strings.Contains(line, "frame=") && strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") { if strings.Contains(line, "frame=") && strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") {
var re = regexp.MustCompile(`=\s+`) 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) dursec, _ := strconv.ParseFloat(t.MediaFile().Metadata().Format.Duration, 64)
//live stream check //live stream check
if dursec != 0 { if dursec != 0 {

View File

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