mirror of
https://github.com/fxkt-tech/liv
synced 2025-09-26 20:11:20 +08:00
feat: concat service
This commit is contained in:
35
examples/service/transcode/concat/main.go
Normal file
35
examples/service/transcode/concat/main.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fxkt-tech/liv"
|
||||||
|
"github.com/fxkt-tech/liv/ffmpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
params = &liv.ConcatParams{
|
||||||
|
Infiles: []string{
|
||||||
|
"../../../testdata/in1.mp4",
|
||||||
|
"../../../testdata/in2.mp4",
|
||||||
|
},
|
||||||
|
ConcatFile: "mylist.txt",
|
||||||
|
Outfile: "out.mp4",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tc := liv.NewTranscode(
|
||||||
|
liv.FFmpegOptions(
|
||||||
|
ffmpeg.Binary("ffmpeg"),
|
||||||
|
ffmpeg.Debug(true),
|
||||||
|
// ffmpeg.Dry(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
err := tc.Concat(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
@@ -4,5 +4,5 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
|
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
|
||||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@@ -8,8 +8,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
46
internal/sugar/mapper.go
Normal file
46
internal/sugar/mapper.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package sugar
|
||||||
|
|
||||||
|
type Single[I, O any] func(I) O
|
||||||
|
|
||||||
|
func Range[I, O any](inS []I, f Single[I, O]) []O {
|
||||||
|
outs := make([]O, len(inS))
|
||||||
|
for i, in := range inS {
|
||||||
|
outs[i] = f(in)
|
||||||
|
}
|
||||||
|
return outs
|
||||||
|
}
|
||||||
|
|
||||||
|
func In[T int | string](elems []T, dest T) bool {
|
||||||
|
for _, elem := range elems {
|
||||||
|
if elem == dest {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func If[F func()](cond bool, f F) {
|
||||||
|
if cond {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filter[T any](slices []T, satisfied func(T) bool) []T {
|
||||||
|
var results []T
|
||||||
|
for _, s := range slices {
|
||||||
|
if satisfied(s) {
|
||||||
|
results = append(results, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapTo[T1, T2 any](slices []T1, deal func(T1) (T2, error)) []T2 {
|
||||||
|
var results []T2
|
||||||
|
for _, s := range slices {
|
||||||
|
if t, err := deal(s); err == nil {
|
||||||
|
results = append(results, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
42
transcode.go
42
transcode.go
@@ -3,6 +3,8 @@ package liv
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/fxkt-tech/liv/ffmpeg"
|
"github.com/fxkt-tech/liv/ffmpeg"
|
||||||
"github.com/fxkt-tech/liv/ffmpeg/codec"
|
"github.com/fxkt-tech/liv/ffmpeg/codec"
|
||||||
@@ -11,6 +13,7 @@ import (
|
|||||||
"github.com/fxkt-tech/liv/ffmpeg/naming"
|
"github.com/fxkt-tech/liv/ffmpeg/naming"
|
||||||
"github.com/fxkt-tech/liv/ffmpeg/output"
|
"github.com/fxkt-tech/liv/ffmpeg/output"
|
||||||
"github.com/fxkt-tech/liv/ffmpeg/util"
|
"github.com/fxkt-tech/liv/ffmpeg/util"
|
||||||
|
"github.com/fxkt-tech/liv/internal/sugar"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transcode struct {
|
type Transcode struct {
|
||||||
@@ -284,6 +287,7 @@ func (tc *Transcode) ConvertContainer(ctx context.Context, params *ConvertContai
|
|||||||
Run(ctx)
|
Run(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转hls
|
||||||
func (tc *Transcode) SimpleHLS(ctx context.Context, params *TranscodeSimpleHLSParams) error {
|
func (tc *Transcode) SimpleHLS(ctx context.Context, params *TranscodeSimpleHLSParams) error {
|
||||||
err := tc.spec.SimpleHLSSatified(params)
|
err := tc.spec.SimpleHLSSatified(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -385,6 +389,7 @@ func (tc *Transcode) SimpleHLS(ctx context.Context, params *TranscodeSimpleHLSPa
|
|||||||
Run(ctx)
|
Run(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转ts
|
||||||
func (tc *Transcode) SimpleTS(ctx context.Context, params *TranscodeSimpleTSParams) error {
|
func (tc *Transcode) SimpleTS(ctx context.Context, params *TranscodeSimpleTSParams) error {
|
||||||
err := tc.spec.SimpleTSSatified(params)
|
err := tc.spec.SimpleTSSatified(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -498,6 +503,43 @@ func (tc *Transcode) SimpleTS(ctx context.Context, params *TranscodeSimpleTSPara
|
|||||||
Run(ctx)
|
Run(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func concatFile(files []string, localPath string) error {
|
||||||
|
f, err := os.Create(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
files = sugar.Range(files, func(f string) string { return fmt.Sprintf("file %s", f) })
|
||||||
|
fs := strings.Join(files, "\n")
|
||||||
|
_, err = f.Write([]byte(fs))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个视频合并成一个
|
||||||
|
func (tc *Transcode) Concat(ctx context.Context, params *ConcatParams) error {
|
||||||
|
err := tc.spec.ConcatSatified(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = concatFile(params.Infiles, params.ConcatFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ffmpeg.NewFFmpeg(
|
||||||
|
ffmpeg.Debug(true),
|
||||||
|
).AddInput(
|
||||||
|
input.WithConcat(params.ConcatFile),
|
||||||
|
).AddOutput(
|
||||||
|
output.New(
|
||||||
|
output.VideoCodec(codec.Copy),
|
||||||
|
output.AudioCodec(codec.Copy),
|
||||||
|
output.Duration(params.Duration),
|
||||||
|
output.File(params.Outfile),
|
||||||
|
),
|
||||||
|
).Run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// ffmpeg -i in.mp4 -vn -c:a copy out.aac
|
// ffmpeg -i in.mp4 -vn -c:a copy out.aac
|
||||||
func (tc *Transcode) ExtractAudio(ctx context.Context, params *ExtractAudioParams) error {
|
func (tc *Transcode) ExtractAudio(ctx context.Context, params *ExtractAudioParams) error {
|
||||||
err := tc.spec.ExtractAudioSatified(params)
|
err := tc.spec.ExtractAudioSatified(params)
|
||||||
|
@@ -30,6 +30,13 @@ type TranscodeSimpleHLSParams struct {
|
|||||||
Threads int32
|
Threads int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConcatParams struct {
|
||||||
|
Infiles []string
|
||||||
|
ConcatFile string // eg. mylist.txt
|
||||||
|
Outfile string
|
||||||
|
Duration float64
|
||||||
|
}
|
||||||
|
|
||||||
type ExtractAudioParams struct {
|
type ExtractAudioParams struct {
|
||||||
Infile string
|
Infile string
|
||||||
Outfile string
|
Outfile string
|
||||||
|
@@ -52,6 +52,14 @@ func (*TranscodeSpec) SimpleTSSatified(params *TranscodeSimpleTSParams) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*TranscodeSpec) ConcatSatified(params *ConcatParams) error {
|
||||||
|
if params == nil || len(params.Infiles) == 0 || params.Outfile == "" {
|
||||||
|
return ErrParamsInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (*TranscodeSpec) ExtractAudioSatified(params *ExtractAudioParams) error {
|
func (*TranscodeSpec) ExtractAudioSatified(params *ExtractAudioParams) error {
|
||||||
if params == nil || params.Infile == "" || params.Outfile == "" {
|
if params == nil || params.Infile == "" || params.Outfile == "" {
|
||||||
return ErrParamsInvalid
|
return ErrParamsInvalid
|
||||||
|
Reference in New Issue
Block a user