mirror of
https://github.com/u2takey/ffmpeg-go.git
synced 2025-10-04 23:52:42 +08:00
init with test
This commit is contained in:
5
README
Normal file
5
README
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# ffmpeg-go
|
||||||
|
|
||||||
|
ffmpeg-go is golang port of https://github.com/kkroening/ffmpeg-python
|
||||||
|
|
||||||
|
check ffmpeg_test.go for examples.
|
152
dag.go
Normal file
152
dag.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node in a directed-acyclic graph (DAG).
|
||||||
|
//
|
||||||
|
// Edges:
|
||||||
|
// DagNodes are connected by edges. An edge connects two nodes with a label for each side:
|
||||||
|
// - ``upstream_node``: upstream/parent node
|
||||||
|
// - ``upstream_label``: label on the outgoing side of the upstream node
|
||||||
|
// - ``downstream_node``: downstream/child node
|
||||||
|
// - ``downstream_label``: label on the incoming side of the downstream node
|
||||||
|
//
|
||||||
|
// For example, DagNode A may be connected to DagNode B with an edge labelled "foo" on A's side, and "bar" on B's
|
||||||
|
// side:
|
||||||
|
//
|
||||||
|
// _____ _____
|
||||||
|
// | | | |
|
||||||
|
// | A >[foo]---[bar]> B |
|
||||||
|
// |_____| |_____|
|
||||||
|
//
|
||||||
|
// Edge labels may be integers or strings, and nodes cannot have more than one incoming edge with the same label.
|
||||||
|
//
|
||||||
|
// DagNodes may have any number of incoming edges and any number of outgoing edges. DagNodes keep track only of
|
||||||
|
// their incoming edges, but the entire graph structure can be inferred by looking at the furthest downstream
|
||||||
|
// nodes and working backwards.
|
||||||
|
//
|
||||||
|
// Hashing:
|
||||||
|
// DagNodes must be hashable, and two nodes are considered to be equivalent if they have the same hash value.
|
||||||
|
//
|
||||||
|
// Nodes are immutable, and the hash should remain constant as a result. If a node with new contents is required,
|
||||||
|
// create a new node and throw the old one away.
|
||||||
|
//
|
||||||
|
// String representation:
|
||||||
|
// In order for graph visualization tools to show useful information, nodes must be representable as strings. The
|
||||||
|
// ``repr`` operator should provide a more or less "full" representation of the node, and the ``short_repr``
|
||||||
|
// property should be a shortened, concise representation.
|
||||||
|
//
|
||||||
|
// Again, because nodes are immutable, the string representations should remain constant.
|
||||||
|
type DagNode interface {
|
||||||
|
Hash() int
|
||||||
|
// Compare two nodes
|
||||||
|
Equal(other DagNode) bool
|
||||||
|
// Return a full string representation of the node.
|
||||||
|
String() string
|
||||||
|
// Return a partial/concise representation of the node
|
||||||
|
ShortRepr() string
|
||||||
|
// Provides information about all incoming edges that connect to this node.
|
||||||
|
//
|
||||||
|
// The edge map is a dictionary that maps an ``incoming_label`` to ``(outgoing_node, outgoing_label)``. Note that
|
||||||
|
// implicity, ``incoming_node`` is ``self``. See "Edges" section above.
|
||||||
|
IncomingEdgeMap() map[Label]NodeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type Label string
|
||||||
|
type NodeInfo struct {
|
||||||
|
Node DagNode
|
||||||
|
Label Label
|
||||||
|
Selector Selector
|
||||||
|
}
|
||||||
|
type Selector string
|
||||||
|
|
||||||
|
type DagEdge struct {
|
||||||
|
DownStreamNode DagNode
|
||||||
|
DownStreamLabel Label
|
||||||
|
UpStreamNode DagNode
|
||||||
|
UpStreamLabel Label
|
||||||
|
UpStreamSelector Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInComingEdges(downStreamNode DagNode, inComingEdgeMap map[Label]NodeInfo) []DagEdge {
|
||||||
|
var edges []DagEdge
|
||||||
|
for _, downStreamLabel := range _getAllLabelsInSorted(inComingEdgeMap) {
|
||||||
|
upStreamInfo := inComingEdgeMap[downStreamLabel]
|
||||||
|
edges = append(edges, DagEdge{
|
||||||
|
DownStreamNode: downStreamNode,
|
||||||
|
DownStreamLabel: downStreamLabel,
|
||||||
|
UpStreamNode: upStreamInfo.Node,
|
||||||
|
UpStreamLabel: upStreamInfo.Label,
|
||||||
|
UpStreamSelector: upStreamInfo.Selector,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return edges
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOutGoingEdges(upStreamNode DagNode, outOutingEdgeMap map[Label][]NodeInfo) []DagEdge {
|
||||||
|
var edges []DagEdge
|
||||||
|
for _, upStreamLabel := range _getAllLabelsSorted(outOutingEdgeMap) {
|
||||||
|
downStreamInfos := outOutingEdgeMap[upStreamLabel]
|
||||||
|
for _, downStreamInfo := range downStreamInfos {
|
||||||
|
edges = append(edges, DagEdge{
|
||||||
|
DownStreamNode: downStreamInfo.Node,
|
||||||
|
DownStreamLabel: downStreamInfo.Label,
|
||||||
|
UpStreamNode: upStreamNode,
|
||||||
|
UpStreamLabel: upStreamLabel,
|
||||||
|
UpStreamSelector: downStreamInfo.Selector,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return edges
|
||||||
|
}
|
||||||
|
|
||||||
|
func TopSort(downStreamNodes []DagNode) (sortedNodes []DagNode, outOutingEdgeMaps map[int]map[Label][]NodeInfo, err error) {
|
||||||
|
markedNodes := map[int]struct{}{}
|
||||||
|
markedSortedNodes := map[int]struct{}{}
|
||||||
|
outOutingEdgeMaps = map[int]map[Label][]NodeInfo{}
|
||||||
|
|
||||||
|
var visit func(upStreamNode, downstreamNode DagNode, upStreamLabel, downStreamLabel Label, downStreamSelector Selector) error
|
||||||
|
visit = func(upStreamNode, downstreamNode DagNode, upStreamLabel, downStreamLabel Label, downStreamSelector Selector) error {
|
||||||
|
if _, ok := markedNodes[upStreamNode.Hash()]; ok {
|
||||||
|
return errors.New("graph if not DAG")
|
||||||
|
}
|
||||||
|
if downstreamNode != nil {
|
||||||
|
if a, ok := outOutingEdgeMaps[upStreamNode.Hash()]; !ok || a == nil {
|
||||||
|
outOutingEdgeMaps[upStreamNode.Hash()] = map[Label][]NodeInfo{}
|
||||||
|
}
|
||||||
|
outgoingEdgeMap := outOutingEdgeMaps[upStreamNode.Hash()]
|
||||||
|
outgoingEdgeMap[upStreamLabel] = append(outgoingEdgeMap[upStreamLabel], NodeInfo{
|
||||||
|
Node: downstreamNode,
|
||||||
|
Label: downStreamLabel,
|
||||||
|
Selector: downStreamSelector,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := markedSortedNodes[upStreamNode.Hash()]; !ok {
|
||||||
|
markedNodes[upStreamNode.Hash()] = struct{}{}
|
||||||
|
for _, edge := range GetInComingEdges(upStreamNode, upStreamNode.IncomingEdgeMap()) {
|
||||||
|
err := visit(edge.UpStreamNode, edge.DownStreamNode, edge.UpStreamLabel, edge.DownStreamLabel, edge.UpStreamSelector)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(markedNodes, upStreamNode.Hash())
|
||||||
|
sortedNodes = append(sortedNodes, upStreamNode)
|
||||||
|
markedSortedNodes[upStreamNode.Hash()] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(downStreamNodes) > 0 {
|
||||||
|
node := downStreamNodes[len(downStreamNodes)-1]
|
||||||
|
downStreamNodes = downStreamNodes[:len(downStreamNodes)-1]
|
||||||
|
err = visit(node, nil, "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
43
debug.go
Normal file
43
debug.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// +build debug
|
||||||
|
|
||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DebugNodes(node []DagNode) {
|
||||||
|
b := strings.Builder{}
|
||||||
|
for _, n := range node {
|
||||||
|
b.WriteString(fmt.Sprintf("%s\n", n.String()))
|
||||||
|
}
|
||||||
|
log.Println(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugOutGoingMap(node []DagNode, m map[int]map[Label][]NodeInfo) {
|
||||||
|
b := strings.Builder{}
|
||||||
|
h := map[int]DagNode{}
|
||||||
|
for _, n := range node {
|
||||||
|
h[n.Hash()] = n
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
b.WriteString(fmt.Sprintf("[Key]: %s", h[k].String()))
|
||||||
|
b.WriteString(" [Value]: {")
|
||||||
|
for l, mm := range v {
|
||||||
|
if l == "" {
|
||||||
|
l = "None"
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf("%s: [", l))
|
||||||
|
for _, x := range mm {
|
||||||
|
b.WriteString(x.Node.String())
|
||||||
|
b.WriteString(", ")
|
||||||
|
}
|
||||||
|
b.WriteString("]")
|
||||||
|
}
|
||||||
|
b.WriteString("}")
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
log.Println(b.String())
|
||||||
|
}
|
5
debugx.go
Normal file
5
debugx.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
func DebugNodes(node []DagNode) {}
|
||||||
|
|
||||||
|
func DebugOutGoingMap(node []DagNode, m map[int]map[Label][]NodeInfo) {}
|
92
ffmpeg.go
Normal file
92
ffmpeg.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Input file URL (ffmpeg ``-i`` option)
|
||||||
|
//
|
||||||
|
// Any supplied kwargs are passed to ffmpeg verbatim (e.g. ``t=20``,
|
||||||
|
// ``f='mp4'``, ``acodec='pcm'``, etc.).
|
||||||
|
//
|
||||||
|
// To tell ffmpeg to read from stdin, use ``pipe:`` as the filename.
|
||||||
|
//
|
||||||
|
// Official documentation: `Main options <https://ffmpeg.org/ffmpeg.html#Main-options>`__
|
||||||
|
func Input(filename string, kwargs ...KwArgs) *Stream {
|
||||||
|
args := MergeKwArgs(kwargs)
|
||||||
|
args["filename"] = filename
|
||||||
|
if fmt := args.PopString("f"); fmt != "" {
|
||||||
|
if args.HasKey("format") {
|
||||||
|
panic(errors.New("can't specify both `format` and `f` options"))
|
||||||
|
}
|
||||||
|
args["format"] = fmt
|
||||||
|
}
|
||||||
|
return NewInputNode("input", nil, args).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add extra global command-line argument(s), e.g. ``-progress``.
|
||||||
|
func (s *Stream) GlobalArgs(args ...string) *Stream {
|
||||||
|
if s.Type != "OutputStream" {
|
||||||
|
panic("cannot overwrite outputs on non-OutputStream")
|
||||||
|
}
|
||||||
|
return NewGlobalNode("global_args", []*Stream{s}, args, nil).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite output files without asking (ffmpeg ``-y`` option)
|
||||||
|
//
|
||||||
|
// Official documentation: `Main options <https://ffmpeg.org/ffmpeg.html#Main-options>`_
|
||||||
|
func (s *Stream) OverwriteOutput(stream *Stream) *Stream {
|
||||||
|
if s.Type != "OutputStream" {
|
||||||
|
panic("cannot overwrite outputs on non-OutputStream")
|
||||||
|
}
|
||||||
|
return NewGlobalNode("overwrite_output", []*Stream{stream}, []string{"-y"}, nil).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include all given outputs in one ffmpeg command line
|
||||||
|
func (s *Stream) MergeOutputs(streams ...*Stream) *Stream {
|
||||||
|
if s.Type != "OutputStream" {
|
||||||
|
panic("cannot merge outputs on non-OutputStream")
|
||||||
|
}
|
||||||
|
return NewMergeOutputsNode("merge_output", streams).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Output(streams []*Stream, fileName string, kwargs ...KwArgs) *Stream {
|
||||||
|
args := MergeKwArgs(kwargs)
|
||||||
|
if !args.HasKey("filename") {
|
||||||
|
if fileName == "" {
|
||||||
|
panic("filename must be provided")
|
||||||
|
}
|
||||||
|
args["filename"] = fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewOutputNode("output", streams, nil, args).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Output file URL
|
||||||
|
//
|
||||||
|
// Syntax:
|
||||||
|
// `ffmpeg.output(stream1[, stream2, stream3...], filename, **ffmpeg_args)`
|
||||||
|
//
|
||||||
|
// Any supplied keyword arguments are passed to ffmpeg verbatim (e.g.
|
||||||
|
// ``t=20``, ``f='mp4'``, ``acodec='pcm'``, ``vcodec='rawvideo'``,
|
||||||
|
// etc.). Some keyword-arguments are handled specially, as shown below.
|
||||||
|
//
|
||||||
|
// Args:
|
||||||
|
// video_bitrate: parameter for ``-b:v``, e.g. ``video_bitrate=1000``.
|
||||||
|
// audio_bitrate: parameter for ``-b:a``, e.g. ``audio_bitrate=200``.
|
||||||
|
// format: alias for ``-f`` parameter, e.g. ``format='mp4'``
|
||||||
|
// (equivalent to ``f='mp4'``).
|
||||||
|
//
|
||||||
|
// If multiple streams are provided, they are mapped to the same
|
||||||
|
// output.
|
||||||
|
//
|
||||||
|
// To tell ffmpeg to write to stdout, use ``pipe:`` as the filename.
|
||||||
|
//
|
||||||
|
// Official documentation: `Synopsis <https://ffmpeg.org/ffmpeg.html#Synopsis>`__
|
||||||
|
// """
|
||||||
|
func (s *Stream) Output(fileName string, kwargs ...KwArgs) *Stream {
|
||||||
|
if s.Type != "FilterableStream" {
|
||||||
|
panic("cannot output on non-FilterableStream")
|
||||||
|
}
|
||||||
|
return Output([]*Stream{s}, fileName, kwargs...)
|
||||||
|
}
|
280
ffmpeg_test.go
Normal file
280
ffmpeg_test.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/u2takey/go-utils/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFluentEquality(t *testing.T) {
|
||||||
|
base1 := Input("dummy1.mp4")
|
||||||
|
base2 := Input("dummy1.mp4")
|
||||||
|
base3 := Input("dummy2.mp4")
|
||||||
|
t1 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 20})
|
||||||
|
t2 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 20})
|
||||||
|
t3 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 30})
|
||||||
|
t4 := base2.Trim(KwArgs{"start_frame": 10, "end_frame": 20})
|
||||||
|
t5 := base3.Trim(KwArgs{"start_frame": 10, "end_frame": 20})
|
||||||
|
|
||||||
|
assert.Equal(t, t1.Hash(), t2.Hash())
|
||||||
|
assert.Equal(t, t1.Hash(), t4.Hash())
|
||||||
|
assert.NotEqual(t, t1.Hash(), t3.Hash())
|
||||||
|
assert.NotEqual(t, t1.Hash(), t5.Hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFluentConcat(t *testing.T) {
|
||||||
|
base1 := Input("dummy1.mp4", nil)
|
||||||
|
trim1 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 20})
|
||||||
|
trim2 := base1.Trim(KwArgs{"start_frame": 30, "end_frame": 40})
|
||||||
|
trim3 := base1.Trim(KwArgs{"start_frame": 50, "end_frame": 60})
|
||||||
|
concat1 := Concat([]*Stream{trim1, trim2, trim3})
|
||||||
|
concat2 := Concat([]*Stream{trim1, trim2, trim3})
|
||||||
|
concat3 := Concat([]*Stream{trim1, trim3, trim2})
|
||||||
|
assert.Equal(t, concat1.Hash(), concat2.Hash())
|
||||||
|
assert.NotEqual(t, concat1.Hash(), concat3.Hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatArgs(t *testing.T) {
|
||||||
|
o := Input("dummy.mp4", nil).Output("dummy2.mp4",
|
||||||
|
KwArgs{"streamid": []string{"0:0x101", "1:0x102"}})
|
||||||
|
assert.Equal(t, o.GetArgs(), []string{"-i", "dummy.mp4", "-streamid", "0:0x101", "-streamid", "1:0x102", "dummy2.mp4"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalArgs(t *testing.T) {
|
||||||
|
o := Input("dummy.mp4", nil).Output("dummy2.mp4", nil).GlobalArgs("-progress", "someurl")
|
||||||
|
|
||||||
|
assert.Equal(t, o.GetArgs(), []string{
|
||||||
|
"-i",
|
||||||
|
"dummy.mp4",
|
||||||
|
"dummy2.mp4",
|
||||||
|
"-progress",
|
||||||
|
"someurl",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleExample(t *testing.T) {
|
||||||
|
err := Input(TestInputFile1, nil).
|
||||||
|
Output(TestOutputFile1, nil).
|
||||||
|
OverWriteOutput().
|
||||||
|
Run()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ComplexFilterExample() *Stream {
|
||||||
|
split := Input(TestInputFile1).VFlip().Split()
|
||||||
|
split0, split1 := split.Get("0"), split.Get("1")
|
||||||
|
overlayFile := Input(TestOverlayFile).Crop(10, 10, 158, 112)
|
||||||
|
return Concat([]*Stream{
|
||||||
|
split0.Trim(KwArgs{"start_frame": 10, "end_frame": 20}),
|
||||||
|
split1.Trim(KwArgs{"start_frame": 30, "end_frame": 40})}).
|
||||||
|
Overlay(overlayFile.HFlip(), "").
|
||||||
|
DrawBox(50, 50, 120, 120, "red", 5).
|
||||||
|
Output(TestOutputFile1).
|
||||||
|
OverWriteOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComplexFilterExample(t *testing.T) {
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
TestInputFile1,
|
||||||
|
"-i",
|
||||||
|
TestOverlayFile,
|
||||||
|
"-filter_complex",
|
||||||
|
"[0]vflip[s0];" +
|
||||||
|
"[s0]split=2[s1][s2];" +
|
||||||
|
"[s1]trim=end_frame=20:start_frame=10[s3];" +
|
||||||
|
"[s2]trim=end_frame=40:start_frame=30[s4];" +
|
||||||
|
"[s3][s4]concat=n=2[s5];" +
|
||||||
|
"[1]crop=158:112:10:10[s6];" +
|
||||||
|
"[s6]hflip[s7];" +
|
||||||
|
"[s5][s7]overlay=eof_action=repeat[s8];" +
|
||||||
|
"[s8]drawbox=50:50:120:120:red:t=5[s9]",
|
||||||
|
"-map",
|
||||||
|
"[s9]",
|
||||||
|
TestOutputFile1,
|
||||||
|
"-y",
|
||||||
|
}, ComplexFilterExample().GetArgs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCombinedOutput(t *testing.T) {
|
||||||
|
i1 := Input(TestInputFile1)
|
||||||
|
i2 := Input(TestOverlayFile)
|
||||||
|
out := Output([]*Stream{i1, i2}, TestOutputFile1)
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
TestInputFile1,
|
||||||
|
"-i",
|
||||||
|
TestOverlayFile,
|
||||||
|
"-map",
|
||||||
|
"0",
|
||||||
|
"-map",
|
||||||
|
"1",
|
||||||
|
TestOutputFile1,
|
||||||
|
}, out.GetArgs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterWithSelector(t *testing.T) {
|
||||||
|
i := Input(TestInputFile1)
|
||||||
|
|
||||||
|
v1 := i.Video().HFlip()
|
||||||
|
a1 := i.Audio().Filter("aecho", []string{"0.8", "0.9", "1000", "0.3"})
|
||||||
|
|
||||||
|
out := Output([]*Stream{a1, v1}, TestOutputFile1)
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
TestInputFile1,
|
||||||
|
"-filter_complex",
|
||||||
|
"[0:a]aecho=0.8:0.9:1000:0.3[s0];[0:v]hflip[s1]",
|
||||||
|
"-map",
|
||||||
|
"[s0]",
|
||||||
|
"-map",
|
||||||
|
"[s1]",
|
||||||
|
TestOutputFile1}, out.GetArgs())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ComplexFilterAsplitExample() *Stream {
|
||||||
|
split := Input(TestInputFile1).VFlip().ASplit()
|
||||||
|
split0 := split.Get("0")
|
||||||
|
split1 := split.Get("1")
|
||||||
|
|
||||||
|
return Concat([]*Stream{
|
||||||
|
split0.Filter("atrim", nil, KwArgs{"start": 10, "end": 20}),
|
||||||
|
split1.Filter("atrim", nil, KwArgs{"start": 30, "end": 40}),
|
||||||
|
}).Output(TestOutputFile1).OverWriteOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterConcatVideoOnly(t *testing.T) {
|
||||||
|
in1 := Input("in1.mp4")
|
||||||
|
in2 := Input("in2.mp4")
|
||||||
|
args := Concat([]*Stream{in1, in2}).Output("out.mp4").GetArgs()
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
"in1.mp4",
|
||||||
|
"-i",
|
||||||
|
"in2.mp4",
|
||||||
|
"-filter_complex",
|
||||||
|
"[0][1]concat=n=2[s0]",
|
||||||
|
"-map",
|
||||||
|
"[s0]",
|
||||||
|
"out.mp4",
|
||||||
|
}, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterConcatAudioOnly(t *testing.T) {
|
||||||
|
in1 := Input("in1.mp4")
|
||||||
|
in2 := Input("in2.mp4")
|
||||||
|
args := Concat([]*Stream{in1, in2}, KwArgs{"v": 0, "a": 1}).Output("out.mp4").GetArgs()
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
"in1.mp4",
|
||||||
|
"-i",
|
||||||
|
"in2.mp4",
|
||||||
|
"-filter_complex",
|
||||||
|
"[0][1]concat=a=1:n=2:v=0[s0]",
|
||||||
|
"-map",
|
||||||
|
"[s0]",
|
||||||
|
"out.mp4",
|
||||||
|
}, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterConcatAudioVideo(t *testing.T) {
|
||||||
|
in1 := Input("in1.mp4")
|
||||||
|
in2 := Input("in2.mp4")
|
||||||
|
joined := Concat([]*Stream{in1.Video(), in1.Audio(), in2.HFlip(), in2.Get("a")}, KwArgs{"v": 1, "a": 1}).Node
|
||||||
|
args := Output([]*Stream{joined.Get("0"), joined.Get("1")}, "out.mp4").GetArgs()
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
"in1.mp4",
|
||||||
|
"-i",
|
||||||
|
"in2.mp4",
|
||||||
|
"-filter_complex",
|
||||||
|
"[1]hflip[s0];[0:v][0:a][s0][1:a]concat=a=1:n=2:v=1[s1][s2]",
|
||||||
|
"-map",
|
||||||
|
"[s1]",
|
||||||
|
"-map",
|
||||||
|
"[s2]",
|
||||||
|
"out.mp4",
|
||||||
|
}, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterASplit(t *testing.T) {
|
||||||
|
out := ComplexFilterAsplitExample()
|
||||||
|
args := out.GetArgs()
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"-i",
|
||||||
|
TestInputFile1,
|
||||||
|
"-filter_complex",
|
||||||
|
"[0]vflip[s0];[s0]asplit=2[s1][s2];[s1]atrim=end=20:start=10[s3];[s2]atrim=end=40:start=30[s4];[s3][s4]concat=n=2[s5]",
|
||||||
|
"-map",
|
||||||
|
"[s5]",
|
||||||
|
TestOutputFile1,
|
||||||
|
"-y",
|
||||||
|
}, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputBitrate(t *testing.T) {
|
||||||
|
args := Input("in").Output("out", KwArgs{"video_bitrate": 1000, "audio_bitrate": 200}).GetArgs()
|
||||||
|
assert.Equal(t, []string{"-i", "in", "-b:v", "1000", "-b:a", "200", "out"}, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputVideoSize(t *testing.T) {
|
||||||
|
args := Input("in").Output("out", KwArgs{"video_size": "320x240"}).GetArgs()
|
||||||
|
assert.Equal(t, []string{"-i", "in", "-video_size", "320x240", "out"}, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompile(t *testing.T) {
|
||||||
|
out := Input("dummy.mp4").Output("dummy2.mp4")
|
||||||
|
assert.Equal(t, out.Compile().Args, []string{"ffmpeg", "-i", "dummy.mp4", "dummy2.mp4"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipe(t *testing.T) {
|
||||||
|
|
||||||
|
width, height := 32, 32
|
||||||
|
frameSize := width * height * 3
|
||||||
|
frameCount, startFrame := 10, 2
|
||||||
|
_, _ = frameCount, frameSize
|
||||||
|
|
||||||
|
out := Input(
|
||||||
|
"pipe:0",
|
||||||
|
KwArgs{
|
||||||
|
"format": "rawvideo",
|
||||||
|
"pixel_format": "rgb24",
|
||||||
|
"video_size": fmt.Sprintf("%dx%d", width, height),
|
||||||
|
"framerate": 10}).
|
||||||
|
Trim(KwArgs{"start_frame": startFrame}).
|
||||||
|
Output("pipe:1", KwArgs{"format": "rawvideo"})
|
||||||
|
|
||||||
|
args := out.GetArgs()
|
||||||
|
assert.Equal(t, args, []string{
|
||||||
|
"-f",
|
||||||
|
"rawvideo",
|
||||||
|
"-video_size",
|
||||||
|
fmt.Sprintf("%dx%d", width, height),
|
||||||
|
"-framerate",
|
||||||
|
"10",
|
||||||
|
"-pixel_format",
|
||||||
|
"rgb24",
|
||||||
|
"-i",
|
||||||
|
"pipe:0",
|
||||||
|
"-filter_complex",
|
||||||
|
"[0]trim=start_frame=2[s0]",
|
||||||
|
"-map",
|
||||||
|
"[s0]",
|
||||||
|
"-f",
|
||||||
|
"rawvideo",
|
||||||
|
"pipe:1",
|
||||||
|
})
|
||||||
|
|
||||||
|
inBuf := bytes.NewBuffer(nil)
|
||||||
|
for i := 0; i < frameSize*frameCount; i++ {
|
||||||
|
inBuf.WriteByte(byte(rand.IntnRange(0, 255)))
|
||||||
|
}
|
||||||
|
outBuf := bytes.NewBuffer(nil)
|
||||||
|
err := out.WithInput(inBuf).WithOutput(outBuf).Run()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, outBuf.Len(), frameSize*(frameCount-startFrame))
|
||||||
|
}
|
134
filters.go
Normal file
134
filters.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AssetType(hasType, expectType string, action string) {
|
||||||
|
if hasType != expectType {
|
||||||
|
panic(fmt.Sprintf("cannot %s on non-%s", action, expectType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilterMultiOutput(streamSpec []*Stream, filterName string, args []string, kwArgs ...KwArgs) *Node {
|
||||||
|
return NewFilterNode(filterName, streamSpec, -1, args, MergeKwArgs(kwArgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filter(streamSpec []*Stream, filterName string, args []string, kwArgs ...KwArgs) *Stream {
|
||||||
|
return FilterMultiOutput(streamSpec, filterName, args, MergeKwArgs(kwArgs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Filter(filterName string, args []string, kwArgs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "filter")
|
||||||
|
return Filter([]*Stream{s}, filterName, args, MergeKwArgs(kwArgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Split() *Node {
|
||||||
|
AssetType(s.Type, "FilterableStream", "split")
|
||||||
|
return NewFilterNode("split", []*Stream{s}, 1, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) ASplit() *Node {
|
||||||
|
AssetType(s.Type, "FilterableStream", "asplit")
|
||||||
|
return NewFilterNode("asplit", []*Stream{s}, 1, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) SetPts(expr string) *Node {
|
||||||
|
AssetType(s.Type, "FilterableStream", "setpts")
|
||||||
|
return NewFilterNode("setpts", []*Stream{s}, 1, []string{expr}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Trim(kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "trim")
|
||||||
|
return NewFilterNode("trim", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Overlay(overlayParentNode *Stream, eofAction string, kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "overlay")
|
||||||
|
if eofAction == "" {
|
||||||
|
eofAction = "repeat"
|
||||||
|
}
|
||||||
|
args := MergeKwArgs(kwargs)
|
||||||
|
args["eof_action"] = eofAction
|
||||||
|
return NewFilterNode("overlay", []*Stream{s, overlayParentNode}, 2, nil, args).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) HFlip(kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "hflip")
|
||||||
|
return NewFilterNode("hflip", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) VFlip(kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "vflip")
|
||||||
|
return NewFilterNode("vflip", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Crop(x, y, w, h int, kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "crop")
|
||||||
|
return NewFilterNode("crop", []*Stream{s}, 1, []string{
|
||||||
|
strconv.Itoa(w), strconv.Itoa(h), strconv.Itoa(x), strconv.Itoa(y),
|
||||||
|
}, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) DrawBox(x, y, w, h int, color string, thickness int, kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "drawbox")
|
||||||
|
args := MergeKwArgs(kwargs)
|
||||||
|
if thickness != 0 {
|
||||||
|
args["t"] = thickness
|
||||||
|
}
|
||||||
|
return NewFilterNode("drawbox", []*Stream{s}, 1, []string{
|
||||||
|
strconv.Itoa(x), strconv.Itoa(y), strconv.Itoa(w), strconv.Itoa(h), color,
|
||||||
|
}, args).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Drawtext(text string, x, y int, escape bool, kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "drawtext")
|
||||||
|
args := MergeKwArgs(kwargs)
|
||||||
|
if escape {
|
||||||
|
text = fmt.Sprintf("%q", text)
|
||||||
|
}
|
||||||
|
if text != "" {
|
||||||
|
args["text"] = text
|
||||||
|
}
|
||||||
|
if x != 0 {
|
||||||
|
args["x"] = x
|
||||||
|
}
|
||||||
|
|
||||||
|
if y != 0 {
|
||||||
|
args["y"] = y
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFilterNode("drawtext", []*Stream{s}, 1, nil, args).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Concat(streams []*Stream, kwargs ...KwArgs) *Stream {
|
||||||
|
args := MergeKwArgs(kwargs)
|
||||||
|
vsc := args.GetDefault("v", 1).(int)
|
||||||
|
asc := args.GetDefault("a", 0).(int)
|
||||||
|
sc := vsc + asc
|
||||||
|
if len(streams)%sc != 0 {
|
||||||
|
panic("streams count not valid")
|
||||||
|
}
|
||||||
|
args["n"] = len(streams) / sc
|
||||||
|
return NewFilterNode("concat", streams, -1, nil, args).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Concat(streams []*Stream, kwargs ...KwArgs) *Stream {
|
||||||
|
return Concat(append(streams, s), MergeKwArgs(kwargs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) ZoomPan(kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "zoompan")
|
||||||
|
return NewFilterNode("zoompan", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Hue(kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "hue")
|
||||||
|
return NewFilterNode("hue", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) ColorChannelMixer(kwargs ...KwArgs) *Stream {
|
||||||
|
AssetType(s.Type, "FilterableStream", "colorchannelmixer")
|
||||||
|
return NewFilterNode("colorchannelmixer", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "")
|
||||||
|
}
|
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module github.com/u2takey/ffmpeg-go
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.4.0
|
||||||
|
github.com/tidwall/gjson v1.6.3
|
||||||
|
github.com/u2takey/go-utils v0.0.0-20200713025200-4704d09fc2c7
|
||||||
|
)
|
44
go.sum
Normal file
44
go.sum
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
github.com/opencontainers/selinux v1.5.2/go.mod h1:yTcKuYAh6R95iDpefGLQaPaRwJFwyzAJufJyiTt7s0g=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/tidwall/gjson v1.6.3 h1:aHoiiem0dr7GHkW001T1SMTJ7X5PvyekH5WX0whWGnI=
|
||||||
|
github.com/tidwall/gjson v1.6.3/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
|
||||||
|
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||||
|
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||||
|
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
|
||||||
|
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/u2takey/go-utils v0.0.0-20200713025200-4704d09fc2c7 h1:PT7mE8HJE1mwaSazrOdSeByJ1FoV33/fHUZrBB+zwVU=
|
||||||
|
github.com/u2takey/go-utils v0.0.0-20200713025200-4704d09fc2c7/go.mod h1:ATqKFpgjUIlhGRs8j59gXmu8Cmpo1QQEHV6vwu1hs28=
|
||||||
|
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
269
node.go
Normal file
269
node.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/u2takey/go-utils/sets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stream struct {
|
||||||
|
Node *Node
|
||||||
|
Label Label
|
||||||
|
Selector Selector
|
||||||
|
Type string
|
||||||
|
Context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStream(node *Node, streamType string, label Label, selector Selector) *Stream {
|
||||||
|
return &Stream{
|
||||||
|
Node: node,
|
||||||
|
Label: label,
|
||||||
|
Selector: selector,
|
||||||
|
Type: streamType,
|
||||||
|
Context: context.Background(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Hash() int {
|
||||||
|
return s.Node.Hash() + getHash(s.Label)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Equal(other Stream) bool {
|
||||||
|
return s.Hash() == other.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) String() string {
|
||||||
|
return fmt.Sprintf("node: %s, label: %s, selector: %s", s.Node.String(), s.Label, s.Selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Get(index string) *Stream {
|
||||||
|
if s.Selector != "" {
|
||||||
|
panic(errors.New("stream already has a selector"))
|
||||||
|
}
|
||||||
|
return s.Node.Stream(s.Label, Selector(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Audio() *Stream {
|
||||||
|
return s.Get("a")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Video() *Stream {
|
||||||
|
return s.Get("v")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStreamMap(streamSpec []*Stream) map[int]*Stream {
|
||||||
|
m := map[int]*Stream{}
|
||||||
|
for i := range streamSpec {
|
||||||
|
m[i] = streamSpec[i]
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStreamMapNodes(streamMap map[int]*Stream) (ret []*Node) {
|
||||||
|
for k := range streamMap {
|
||||||
|
ret = append(ret, streamMap[k].Node)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStreamSpecNodes(streamSpec []*Stream) []*Node {
|
||||||
|
return getStreamMapNodes(getStreamMap(streamSpec))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
streamSpec []*Stream
|
||||||
|
name string
|
||||||
|
incomingStreamTypes sets.String
|
||||||
|
outgoingStreamType string
|
||||||
|
minInputs int
|
||||||
|
maxInputs int
|
||||||
|
args []string
|
||||||
|
kwargs KwArgs
|
||||||
|
nodeType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNode(streamSpec []*Stream,
|
||||||
|
name string,
|
||||||
|
incomingStreamTypes sets.String,
|
||||||
|
outgoingStreamType string,
|
||||||
|
minInputs int,
|
||||||
|
maxInputs int,
|
||||||
|
args []string,
|
||||||
|
kwargs KwArgs,
|
||||||
|
nodeType string) *Node {
|
||||||
|
n := &Node{
|
||||||
|
streamSpec: streamSpec,
|
||||||
|
name: name,
|
||||||
|
incomingStreamTypes: incomingStreamTypes,
|
||||||
|
outgoingStreamType: outgoingStreamType,
|
||||||
|
minInputs: minInputs,
|
||||||
|
maxInputs: maxInputs,
|
||||||
|
args: args,
|
||||||
|
kwargs: kwargs,
|
||||||
|
nodeType: nodeType,
|
||||||
|
}
|
||||||
|
n.__checkInputLen()
|
||||||
|
n.__checkInputTypes()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInputNode(name string, args []string, kwargs KwArgs) *Node {
|
||||||
|
return NewNode(nil,
|
||||||
|
name,
|
||||||
|
nil,
|
||||||
|
"FilterableStream",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
"InputNode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilterNode(name string, streamSpec []*Stream, maxInput int, args []string, kwargs KwArgs) *Node {
|
||||||
|
return NewNode(streamSpec,
|
||||||
|
name,
|
||||||
|
sets.NewString("FilterableStream"),
|
||||||
|
"FilterableStream",
|
||||||
|
1,
|
||||||
|
maxInput,
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
"FilterNode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOutputNode(name string, streamSpec []*Stream, args []string, kwargs KwArgs) *Node {
|
||||||
|
return NewNode(streamSpec,
|
||||||
|
name,
|
||||||
|
sets.NewString("FilterableStream"),
|
||||||
|
"OutputStream",
|
||||||
|
1,
|
||||||
|
-1,
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
"OutputNode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMergeOutputsNode(name string, streamSpec []*Stream) *Node {
|
||||||
|
return NewNode(streamSpec,
|
||||||
|
name,
|
||||||
|
sets.NewString("OutputStream"),
|
||||||
|
"OutputStream",
|
||||||
|
1,
|
||||||
|
-1,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"MergeOutputsNode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGlobalNode(name string, streamSpec []*Stream, args []string, kwargs KwArgs) *Node {
|
||||||
|
return NewNode(streamSpec,
|
||||||
|
name,
|
||||||
|
sets.NewString("OutputStream"),
|
||||||
|
"OutputStream",
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
"GlobalNode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) __checkInputLen() {
|
||||||
|
streamMap := getStreamMap(n.streamSpec)
|
||||||
|
if n.minInputs >= 0 && len(streamMap) < n.minInputs {
|
||||||
|
panic(fmt.Sprintf("Expected at least %d input stream(s); got %d", n.minInputs, len(streamMap)))
|
||||||
|
}
|
||||||
|
if n.maxInputs >= 0 && len(streamMap) > n.maxInputs {
|
||||||
|
panic(fmt.Sprintf("Expected at most %d input stream(s); got %d", n.maxInputs, len(streamMap)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) __checkInputTypes() {
|
||||||
|
streamMap := getStreamMap(n.streamSpec)
|
||||||
|
for _, s := range streamMap {
|
||||||
|
if !n.incomingStreamTypes.Has(s.Type) {
|
||||||
|
panic(fmt.Sprintf("Expected incoming stream(s) to be of one of the following types: %s; got %s", n.incomingStreamTypes, s.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) __getIncomingEdgeMap() map[Label]NodeInfo {
|
||||||
|
incomingEdgeMap := map[Label]NodeInfo{}
|
||||||
|
streamMap := getStreamMap(n.streamSpec)
|
||||||
|
for i, s := range streamMap {
|
||||||
|
incomingEdgeMap[Label(fmt.Sprintf("%v", i))] = NodeInfo{
|
||||||
|
Node: s.Node,
|
||||||
|
Label: s.Label,
|
||||||
|
Selector: s.Selector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return incomingEdgeMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Hash() int {
|
||||||
|
b := 0
|
||||||
|
for downStreamLabel, upStreamInfo := range n.IncomingEdgeMap() {
|
||||||
|
b += getHash(fmt.Sprintf("%s%d%s%s", downStreamLabel, upStreamInfo.Node.Hash(), upStreamInfo.Label, upStreamInfo.Selector))
|
||||||
|
}
|
||||||
|
b += getHash(n.args)
|
||||||
|
b += getHash(n.kwargs)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) String() string {
|
||||||
|
return fmt.Sprintf("%s (%s) <%s>", n.name, getString(n.args), getString(n.kwargs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Equal(other DagNode) bool {
|
||||||
|
return n.Hash() == other.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) ShortRepr() string {
|
||||||
|
return n.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) IncomingEdgeMap() map[Label]NodeInfo {
|
||||||
|
return n.__getIncomingEdgeMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) GetInComingEdges() []DagEdge {
|
||||||
|
return GetInComingEdges(n, n.IncomingEdgeMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Stream(label Label, selector Selector) *Stream {
|
||||||
|
return NewStream(n, n.outgoingStreamType, label, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Get(a string) *Stream {
|
||||||
|
l := strings.Split(a, ":")
|
||||||
|
if len(l) == 2 {
|
||||||
|
return n.Stream(Label(l[0]), Selector(l[1]))
|
||||||
|
}
|
||||||
|
return n.Stream(Label(a), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) GetFilter(outgoingEdges []DagEdge) string {
|
||||||
|
if n.nodeType != "FilterNode" {
|
||||||
|
panic("call GetFilter on non-FilterNode")
|
||||||
|
}
|
||||||
|
args := n.args
|
||||||
|
kwargs := n.kwargs
|
||||||
|
if n.name == "split" || n.name == "asplit" {
|
||||||
|
args = []string{fmt.Sprintf("%d", len(outgoingEdges))}
|
||||||
|
}
|
||||||
|
// todo escape char
|
||||||
|
for _, k := range kwargs.SortedKeys() {
|
||||||
|
v := getString(kwargs[k])
|
||||||
|
if v != "" {
|
||||||
|
args = append(args, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
} else {
|
||||||
|
args = append(args, fmt.Sprintf("%s", k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Sprintf("%s=%s", n.name, strings.Join(args, ":"))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s", n.name)
|
||||||
|
}
|
40
probe.go
Normal file
40
probe.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run ffprobe on the specified file and return a JSON representation of the output.
|
||||||
|
//
|
||||||
|
// Raises:
|
||||||
|
// :class:`ffmpeg.Error`: if ffprobe returns a non-zero exit code,
|
||||||
|
// an :class:`Error` is returned with a generic error message.
|
||||||
|
// The stderr output can be retrieved by accessing the
|
||||||
|
// ``stderr`` property of the exception.
|
||||||
|
|
||||||
|
func Probe(fileName string, kwargs KwArgs) (string, error) {
|
||||||
|
return ProbeWithTimeout(fileName, 0, kwargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProbeWithTimeout(fileName string, timeOut time.Duration, kwargs KwArgs) (string, error) {
|
||||||
|
args := []string{"-show_format", "-show_streams", "-of", "json"}
|
||||||
|
args = append(args, ConvertKwargsToCmdLineArgs(kwargs)...)
|
||||||
|
args = append(args, fileName)
|
||||||
|
ctx := context.Background()
|
||||||
|
if timeOut > 0 {
|
||||||
|
var cancel func()
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), timeOut)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "ffprobe", args...)
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
cmd.Stdout = buf
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(buf.Bytes()), nil
|
||||||
|
}
|
20
probe_test.go
Normal file
20
probe_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TestInputFile1 = "./sample_data/in1.mp4"
|
||||||
|
TestOutputFile1 = "./sample_data/out1.mp4"
|
||||||
|
TestOverlayFile = "./sample_data/overlay.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProbe(t *testing.T) {
|
||||||
|
data, err := Probe(TestInputFile1, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, gjson.Get(data, "format.duration").String(), "7.036000")
|
||||||
|
}
|
249
run.go
Normal file
249
run.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInputArgs(node *Node) []string {
|
||||||
|
var args []string
|
||||||
|
if node.name == "input" {
|
||||||
|
kwargs := node.kwargs.Copy()
|
||||||
|
filename := kwargs.PopString("filename")
|
||||||
|
format := kwargs.PopString("format")
|
||||||
|
videoSize := kwargs.PopString("video_size")
|
||||||
|
if format != "" {
|
||||||
|
args = append(args, "-f", format)
|
||||||
|
}
|
||||||
|
if videoSize != "" {
|
||||||
|
args = append(args, "-video_size", videoSize)
|
||||||
|
}
|
||||||
|
args = append(args, ConvertKwargsToCmdLineArgs(kwargs)...)
|
||||||
|
args = append(args, "-i", filename)
|
||||||
|
} else {
|
||||||
|
panic("unsupported node input name")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatInputStreamName(streamNameMap map[string]string, edge DagEdge, finalArg bool) string {
|
||||||
|
prefix := streamNameMap[fmt.Sprintf("%d%s", edge.UpStreamNode.Hash(), edge.UpStreamLabel)]
|
||||||
|
suffix := ""
|
||||||
|
format := "[%s%s]"
|
||||||
|
if edge.UpStreamSelector != "" {
|
||||||
|
suffix = fmt.Sprintf(":%s", edge.UpStreamSelector)
|
||||||
|
}
|
||||||
|
if finalArg && edge.UpStreamNode.(*Node).nodeType == "InputNode" {
|
||||||
|
format = "%s%s"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(format, prefix, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatOutStreamName(streamNameMap map[string]string, edge DagEdge) string {
|
||||||
|
return fmt.Sprintf("[%s]", streamNameMap[fmt.Sprintf("%d%s", edge.UpStreamNode.Hash(), edge.UpStreamLabel)])
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getFilterSpec(node *Node, outOutingEdgeMap map[Label][]NodeInfo, streamNameMap map[string]string) string {
|
||||||
|
var input, output []string
|
||||||
|
for _, e := range node.GetInComingEdges() {
|
||||||
|
input = append(input, formatInputStreamName(streamNameMap, e, false))
|
||||||
|
}
|
||||||
|
outEdges := GetOutGoingEdges(node, outOutingEdgeMap)
|
||||||
|
for _, e := range outEdges {
|
||||||
|
output = append(output, formatOutStreamName(streamNameMap, e))
|
||||||
|
}
|
||||||
|
//sort.Strings(input)
|
||||||
|
//sort.Strings(output)
|
||||||
|
return fmt.Sprintf("%s%s%s", strings.Join(input, ""), node.GetFilter(outEdges), strings.Join(output, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getAllLabelsInSorted(m map[Label]NodeInfo) []Label {
|
||||||
|
var r []Label
|
||||||
|
for a := range m {
|
||||||
|
r = append(r, a)
|
||||||
|
}
|
||||||
|
sort.Slice(r, func(i, j int) bool {
|
||||||
|
return r[i] < r[j]
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getAllLabelsSorted(m map[Label][]NodeInfo) []Label {
|
||||||
|
var r []Label
|
||||||
|
for a := range m {
|
||||||
|
r = append(r, a)
|
||||||
|
}
|
||||||
|
sort.Slice(r, func(i, j int) bool {
|
||||||
|
return r[i] < r[j]
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func _allocateFilterStreamNames(nodes []*Node, outOutingEdgeMaps map[int]map[Label][]NodeInfo, streamNameMap map[string]string) {
|
||||||
|
sc := 0
|
||||||
|
for _, n := range nodes {
|
||||||
|
om := outOutingEdgeMaps[n.Hash()]
|
||||||
|
// todo sort
|
||||||
|
for _, l := range _getAllLabelsSorted(om) {
|
||||||
|
if len(om[l]) > 1 {
|
||||||
|
panic(fmt.Sprintf(`encountered %s with multiple outgoing edges
|
||||||
|
with same upstream label %s; a 'split'' filter is probably required`, n.name, l))
|
||||||
|
}
|
||||||
|
streamNameMap[fmt.Sprintf("%d%s", n.Hash(), l)] = fmt.Sprintf("s%d", sc)
|
||||||
|
sc += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getFilterArg(nodes []*Node, outOutingEdgeMaps map[int]map[Label][]NodeInfo, streamNameMap map[string]string) string {
|
||||||
|
_allocateFilterStreamNames(nodes, outOutingEdgeMaps, streamNameMap)
|
||||||
|
var filterSpec []string
|
||||||
|
for _, n := range nodes {
|
||||||
|
filterSpec = append(filterSpec, _getFilterSpec(n, outOutingEdgeMaps[n.Hash()], streamNameMap))
|
||||||
|
}
|
||||||
|
return strings.Join(filterSpec, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getGlobalArgs(node *Node) []string {
|
||||||
|
return node.args
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getOutputArgs(node *Node, streamNameMap map[string]string) []string {
|
||||||
|
if node.name != "output" {
|
||||||
|
panic("Unsupported output node")
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
if len(node.GetInComingEdges()) == 0 {
|
||||||
|
panic("Output node has no mapped streams")
|
||||||
|
}
|
||||||
|
for _, e := range node.GetInComingEdges() {
|
||||||
|
streamName := formatInputStreamName(streamNameMap, e, true)
|
||||||
|
if streamName != "0" || len(node.GetInComingEdges()) > 1 {
|
||||||
|
args = append(args, "-map", streamName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kwargs := node.kwargs.Copy()
|
||||||
|
|
||||||
|
filename := kwargs.PopString("filename")
|
||||||
|
if kwargs.HasKey("format") {
|
||||||
|
args = append(args, "-f", kwargs.PopString("format"))
|
||||||
|
}
|
||||||
|
if kwargs.HasKey("video_bitrate") {
|
||||||
|
args = append(args, "-b:v", kwargs.PopString("video_bitrate"))
|
||||||
|
}
|
||||||
|
if kwargs.HasKey("audio_bitrate") {
|
||||||
|
args = append(args, "-b:a", kwargs.PopString("audio_bitrate"))
|
||||||
|
}
|
||||||
|
if kwargs.HasKey("video_size") {
|
||||||
|
args = append(args, "-video_size", kwargs.PopString("video_size"))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, ConvertKwargsToCmdLineArgs(kwargs)...)
|
||||||
|
args = append(args, filename)
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) GetArgs() []string {
|
||||||
|
var args []string
|
||||||
|
nodes := getStreamSpecNodes([]*Stream{s})
|
||||||
|
var dagNodes []DagNode
|
||||||
|
streamNameMap := map[string]string{}
|
||||||
|
for i := range nodes {
|
||||||
|
dagNodes = append(dagNodes, nodes[i])
|
||||||
|
}
|
||||||
|
sorted, outGoingMap, err := TopSort(dagNodes)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
DebugNodes(sorted)
|
||||||
|
DebugOutGoingMap(sorted, outGoingMap)
|
||||||
|
var inputNodes, outputNodes, globalNodes, filterNodes []*Node
|
||||||
|
for i := range sorted {
|
||||||
|
n := sorted[i].(*Node)
|
||||||
|
switch n.nodeType {
|
||||||
|
case "InputNode":
|
||||||
|
streamNameMap[fmt.Sprintf("%d", n.Hash())] = fmt.Sprintf("%d", len(inputNodes))
|
||||||
|
inputNodes = append(inputNodes, n)
|
||||||
|
case "OutputNode":
|
||||||
|
outputNodes = append(outputNodes, n)
|
||||||
|
case "GlobalNode":
|
||||||
|
globalNodes = append(globalNodes, n)
|
||||||
|
case "FilterNode":
|
||||||
|
filterNodes = append(filterNodes, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range inputNodes {
|
||||||
|
args = append(args, getInputArgs(n)...)
|
||||||
|
}
|
||||||
|
filterArgs := _getFilterArg(filterNodes, outGoingMap, streamNameMap)
|
||||||
|
if filterArgs != "" {
|
||||||
|
args = append(args, "-filter_complex", filterArgs)
|
||||||
|
}
|
||||||
|
for _, n := range outputNodes {
|
||||||
|
args = append(args, _getOutputArgs(n, streamNameMap)...)
|
||||||
|
}
|
||||||
|
for _, n := range globalNodes {
|
||||||
|
args = append(args, _getGlobalArgs(n)...)
|
||||||
|
}
|
||||||
|
if s.Context.Value("OverWriteOutput") != nil {
|
||||||
|
args = append(args, "-y")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) WithTimeout(timeOut time.Duration) *Stream {
|
||||||
|
if timeOut > 0 {
|
||||||
|
s.Context, _ = context.WithTimeout(s.Context, timeOut)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) OverWriteOutput() *Stream {
|
||||||
|
s.Context = context.WithValue(s.Context, "OverWriteOutput", struct{}{})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) WithInput(reader io.Reader) *Stream {
|
||||||
|
s.Context = context.WithValue(s.Context, "Stdin", reader)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) WithOutput(out ...io.Writer) *Stream {
|
||||||
|
if len(out) > 0 {
|
||||||
|
s.Context = context.WithValue(s.Context, "Stdout", out[0])
|
||||||
|
}
|
||||||
|
if len(out) > 1 {
|
||||||
|
s.Context = context.WithValue(s.Context, "Stderr", out[1])
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// for test
|
||||||
|
func (s *Stream) Compile() *exec.Cmd {
|
||||||
|
args := s.GetArgs()
|
||||||
|
cmd := exec.CommandContext(s.Context, "ffmpeg", args...)
|
||||||
|
if a, ok := s.Context.Value("Stdin").(io.Reader); ok {
|
||||||
|
cmd.Stdin = a
|
||||||
|
}
|
||||||
|
if a, ok := s.Context.Value("Stdout").(io.Writer); ok {
|
||||||
|
cmd.Stdout = a
|
||||||
|
}
|
||||||
|
if a, ok := s.Context.Value("Stderr").(io.Writer); ok {
|
||||||
|
cmd.Stderr = a
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Run() error {
|
||||||
|
err := s.Compile().Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
BIN
sample_data/in1.mp4
Normal file
BIN
sample_data/in1.mp4
Normal file
Binary file not shown.
BIN
sample_data/out1.mp4
Normal file
BIN
sample_data/out1.mp4
Normal file
Binary file not shown.
BIN
sample_data/overlay.png
Normal file
BIN
sample_data/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
183
utils.go
Normal file
183
utils.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package ffmpeg_go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getString(item interface{}) string {
|
||||||
|
if a, ok := item.(interface{ String() string }); ok {
|
||||||
|
return a.String()
|
||||||
|
}
|
||||||
|
switch a := item.(type) {
|
||||||
|
case string:
|
||||||
|
return a
|
||||||
|
case []string:
|
||||||
|
return strings.Join(a, ", ")
|
||||||
|
case []interface{}:
|
||||||
|
var r []string
|
||||||
|
for _, b := range a {
|
||||||
|
r = append(r, getString(b))
|
||||||
|
}
|
||||||
|
return strings.Join(r, ", ")
|
||||||
|
case KwArgs:
|
||||||
|
var keys, r []string
|
||||||
|
for k := range a {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
r = append(r, fmt.Sprintf("%s: %s", k, getString(a[k])))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{%s}", strings.Join(r, ", "))
|
||||||
|
case map[string]interface{}:
|
||||||
|
var keys, r []string
|
||||||
|
for k := range a {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
r = append(r, fmt.Sprintf("%s: %s", k, getString(a[k])))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{%s}", strings.Join(r, ", "))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHash(item interface{}) int {
|
||||||
|
h := fnv.New64()
|
||||||
|
switch a := item.(type) {
|
||||||
|
case interface{ Hash() int }:
|
||||||
|
return a.Hash()
|
||||||
|
case string:
|
||||||
|
_, _ = h.Write([]byte(a))
|
||||||
|
return int(h.Sum64())
|
||||||
|
case []byte:
|
||||||
|
_, _ = h.Write(a)
|
||||||
|
return int(h.Sum64())
|
||||||
|
case map[string]interface{}:
|
||||||
|
b := 0
|
||||||
|
for k, v := range a {
|
||||||
|
b += getHash(k) + getHash(v)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
case KwArgs:
|
||||||
|
b := 0
|
||||||
|
for k, v := range a {
|
||||||
|
b += getHash(k) + getHash(v)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
default:
|
||||||
|
_, _ = h.Write([]byte(getString(item)))
|
||||||
|
return int(h.Sum64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//def escape_chars(text, chars):
|
||||||
|
// """Helper function to escape uncomfortable characters."""
|
||||||
|
// text = str(text)
|
||||||
|
// chars = list(set(chars))
|
||||||
|
// if '\\' in chars:
|
||||||
|
// chars.remove('\\')
|
||||||
|
// chars.insert(0, '\\')
|
||||||
|
// for ch in chars:
|
||||||
|
// text = text.replace(ch, '\\' + ch)
|
||||||
|
// return text
|
||||||
|
//
|
||||||
|
|
||||||
|
type KwArgs map[string]interface{}
|
||||||
|
|
||||||
|
func MergeKwArgs(args []KwArgs) KwArgs {
|
||||||
|
a := KwArgs{}
|
||||||
|
for _, b := range args {
|
||||||
|
for c := range b {
|
||||||
|
a[c] = b[c]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KwArgs) Copy() KwArgs {
|
||||||
|
r := KwArgs{}
|
||||||
|
for k := range a {
|
||||||
|
r[k] = a[k]
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KwArgs) SortedKeys() []string {
|
||||||
|
var r []string
|
||||||
|
for k := range a {
|
||||||
|
r = append(r, k)
|
||||||
|
}
|
||||||
|
sort.Strings(r)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KwArgs) GetString(k string) string {
|
||||||
|
if v, ok := a[k]; ok {
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KwArgs) PopString(k string) string {
|
||||||
|
if c, ok := a[k]; ok {
|
||||||
|
defer delete(a, k)
|
||||||
|
return fmt.Sprintf("%v", c)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KwArgs) HasKey(k string) bool {
|
||||||
|
_, ok := a[k]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KwArgs) GetDefault(k string, defaultV interface{}) interface{} {
|
||||||
|
if v, ok := a[k]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultV
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertKwargsToCmdLineArgs(kwargs KwArgs) []string {
|
||||||
|
var keys, args []string
|
||||||
|
for k := range kwargs {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
v := kwargs[k]
|
||||||
|
switch a := v.(type) {
|
||||||
|
case string:
|
||||||
|
args = append(args, fmt.Sprintf("-%s", k))
|
||||||
|
if a != "" {
|
||||||
|
args = append(args, a)
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
for _, r := range a {
|
||||||
|
args = append(args, fmt.Sprintf("-%s", k))
|
||||||
|
if r != "" {
|
||||||
|
args = append(args, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []int:
|
||||||
|
for _, r := range a {
|
||||||
|
args = append(args, fmt.Sprintf("-%s", k))
|
||||||
|
args = append(args, strconv.Itoa(r))
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
args = append(args, fmt.Sprintf("-%s", k))
|
||||||
|
args = append(args, strconv.Itoa(a))
|
||||||
|
default:
|
||||||
|
args = append(args, fmt.Sprintf("-%s", k))
|
||||||
|
args = append(args, fmt.Sprintf("%v", a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
Reference in New Issue
Block a user