feat(ffcut): video track

This commit is contained in:
Justyer
2024-04-07 03:08:42 +08:00
parent c9d38559f5
commit e74e94a3a6
20 changed files with 784 additions and 56 deletions

10
ffcut/shelf/errors.go Normal file
View File

@@ -0,0 +1,10 @@
package shelf
import "errors"
var (
// 轨道类型不存在
ErrTrackTypeNotFound = errors.New("track type not found")
// 元素与轨道不匹配
ErrTrackItemTypeNotMatch = errors.New("track and track_type not match")
)

38
ffcut/shelf/operation.go Normal file
View File

@@ -0,0 +1,38 @@
package shelf
// 元素操作
type OpParams interface{}
// 操作
type Operation struct {
Type string `json:"type,omitempty"`
Params OpParams `json:"params,omitempty"`
}
// 旋转
type OpParamsImageRotate struct {
Angle int32 `json:"angle"`
}
func WithOpParamsImageRotate(angle int32) *Operation {
return &Operation{
Type: "image_rotate",
Params: &OpParamsImageRotate{
Angle: angle,
},
}
}
// 调整音量
type OpParamsAudioVolumes struct {
All int32 `json:"all"`
}
func WithOpParamsAudioVolumes(all int32) *Operation {
return &Operation{
Type: "audio_volumes",
Params: &OpParamsAudioVolumes{
All: all,
},
}
}

9
ffcut/shelf/options.go Normal file
View File

@@ -0,0 +1,9 @@
package shelf
type ShelfOption func(*TrackData)
func WithStageSize(w, h int32) ShelfOption {
return func(td *TrackData) {
td.stageWidth, td.stageHeight = w, h
}
}

270
ffcut/shelf/track.go Normal file
View File

@@ -0,0 +1,270 @@
package shelf
import (
"context"
"fmt"
"github.com/fxkt-tech/liv/ffmpeg"
"github.com/fxkt-tech/liv/ffmpeg/codec"
"github.com/fxkt-tech/liv/ffmpeg/filter"
"github.com/fxkt-tech/liv/ffmpeg/input"
"github.com/fxkt-tech/liv/ffmpeg/output"
"github.com/fxkt-tech/liv/internal/conv"
"github.com/fxkt-tech/liv/internal/encoding/json"
"github.com/fxkt-tech/liv/internal/sugar"
"github.com/google/uuid"
)
// 轨道排序优先级,数字小的靠前
var TrackTypeSortMap = map[TrackType]int{
TrackTypeTitle: 1,
TrackTypeFrame: 2,
TrackTypeSubtitle: 3,
TrackTypeImage: 4,
TrackTypeAudio: 5,
TrackTypeVideo: 6,
}
// 轨道类型
type TrackType string
const (
// 文字轨道
TrackTypeTitle TrackType = "title"
// TODO
TrackTypeFrame TrackType = "frame"
// 字幕轨道
TrackTypeSubtitle TrackType = "subtitle"
// TODO
TrackTypeImage TrackType = "image"
// 音频轨道
TrackTypeAudio TrackType = "audio"
// 视频轨道
TrackTypeVideo TrackType = "video"
)
var (
TrackTitleAllowdItems = []TrackItemType{TrackItemTypeTitle, TrackItemTypeAdvancedTitle, TrackItemTypeSequenceTitle, TrackItemTypePAGTitle}
TrackSubtitleAllowdItems = []TrackItemType{TrackItemTypeSubtitle}
TrackAudioAllowdItems = []TrackItemType{TrackItemTypeAudio}
TrackVideoAllowdItems = []TrackItemType{TrackItemTypeVideo, TrackItemTypeImage, TrackItemTypeTransition}
)
// 合成协议
type TrackData struct {
stageWidth int32
stageHeight int32
tracks []*Track
err error
ctx context.Context
}
var New = NewTrackData
func NewTrackData(opts ...ShelfOption) *TrackData {
d := &TrackData{ctx: context.Background()}
sugar.Range(opts, func(opt ShelfOption) { opt(d) })
return d
}
// 获取指定轨道返回nil则表示不存在
func (d *TrackData) GetTrack(trackType TrackType, idx int) *Track {
i := 0
for _, track := range d.tracks {
if track.Type == trackType {
if i == idx {
return track
}
i++
}
}
return nil
}
// 添加轨道
func (d *TrackData) AddTrack(track *Track) *TrackData {
if d.err != nil {
return d
}
d.tracks = append(d.tracks, track)
return d
}
func (d *TrackData) AppendTrack(tracks ...*Track) *TrackData {
for _, track := range tracks {
d.AddTrack(track)
}
return d
}
// 轨道排序,使用稳定性排序
func (d *TrackData) Sort() {
n := len(d.tracks)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if TrackTypeSortMap[d.tracks[j].Type] > TrackTypeSortMap[d.tracks[j+1].Type] {
d.tracks[j], d.tracks[j+1] = d.tracks[j+1], d.tracks[j]
}
}
}
}
func (d *TrackData) Exec() error {
var (
ff = ffmpeg.New(
// ffmpeg.WithDry(true),
ffmpeg.WithDebug(true),
)
// 辅助性质的背景板,用于视频流
bg = filter.Color("black", d.stageWidth, d.stageHeight, 5)
// 舞台
stage = bg
// 音响
// sound *filter.SingleFilter
)
ff.AddFilter(bg)
for i := len(d.tracks) - 1; i >= 0; i-- {
switch d.tracks[i].Type {
case TrackTypeVideo:
for _, item := range d.tracks[i].Items {
iVideo := input.WithTime(conv.MillToF32(item.StartTime), conv.MillToF32(item.Duration), item.AssetId)
// iVideo := input.WithSimple(item.AssetId)
// fTrim := filter.Trim(conv.MillToF32(item.Section.From), conv.MillToF32(item.Section.To)).Use(iVideo.V())
fSetPTS := filter.SetPTS(fmt.Sprintf("PTS+%f/TB", conv.MillToF32(item.Section.From)))
fOverlay := filter.OverlayWithEnable(
item.Position.X, item.Position.Y,
fmt.Sprintf("between(t,%f,%f)", conv.MillToF32(item.Section.From), conv.MillToF32(item.Section.To)),
).Use(stage, fSetPTS)
ff.AddInput(iVideo)
ff.AddFilter(fSetPTS, fOverlay)
// 每完成一步的结果就是当前舞台的模样
stage = fOverlay
}
case TrackTypeAudio:
case TrackTypeTitle:
case TrackTypeSubtitle:
}
}
ff.AddOutput(output.New(
output.Map(stage),
// output.Map(sound),
output.VideoCodec(codec.X264),
// output.AudioCodec(codec.AAC),
output.AudioCodec(codec.Nope),
output.File("out_test.mp4"),
))
err := ff.Run(d.ctx)
if err != nil {
return err
}
return nil
}
// 导出合成协议
func (d *TrackData) Export() (string, error) {
if d.err != nil {
return "", d.err
}
// 处理字幕样式
for _, track := range d.tracks {
if track.Type == TrackTypeSubtitle {
if len(track.Styles) > 0 {
for _, tItem := range track.Items {
tItem.TextStyleId = track.Styles[0].Id
}
}
}
}
d.Sort()
return json.ToString(d.tracks), nil
}
// --- 轨道 ---
type TrackBase struct {
Id string `json:"id"` // 根据uuid生成只要单个合成协议不重复就行
Type TrackType `json:"type"`
}
type Style struct {
Id string `json:"id"`
TextStyle *TextStyle `json:"text_style"`
}
// 轨道
// 轨道类型: title、frame、subtitle、image、audio、video
type Track struct {
TrackBase
Items []*TrackItem `json:"items,omitempty"`
Styles []*Style `json:"styles,omitempty"`
allowedTrackItems []TrackItemType `json:"-"`
err error `json:"-"`
}
// 创建轨道
// 暂时仅支持title/subtitle/audio/video
func NewTrack(trackType TrackType) *Track {
var (
base = TrackBase{
Id: uuid.NewString(),
Type: trackType,
}
allowedTrackItems []TrackItemType
err error
)
switch trackType {
case TrackTypeTitle:
allowedTrackItems = TrackTitleAllowdItems
case TrackTypeSubtitle:
allowedTrackItems = TrackSubtitleAllowdItems
case TrackTypeAudio:
allowedTrackItems = TrackAudioAllowdItems
case TrackTypeVideo:
allowedTrackItems = TrackVideoAllowdItems
default:
err = ErrTrackTypeNotFound
}
return &Track{
TrackBase: base,
allowedTrackItems: allowedTrackItems,
err: err,
}
}
// 向轨道中添加元素
func (t *Track) Push(trackItem *TrackItem) *Track {
if t.err != nil {
return t
}
if !sugar.In(t.allowedTrackItems, trackItem.Type) {
t.err = ErrTrackItemTypeNotMatch
return t
}
t.Items = append(t.Items, trackItem)
return t
}
func (t *Track) Append(trackItems ...*TrackItem) *Track {
for _, trackItem := range trackItems {
t.Push(trackItem)
}
return t
}
func (t *Track) SetStyles(styles ...*Style) *Track {
t.Styles = append(t.Styles, styles...)
return t
}

208
ffcut/shelf/track_item.go Normal file
View File

@@ -0,0 +1,208 @@
package shelf
import "github.com/google/uuid"
// 轨道元素类型
type TrackItemType string
const (
// 普通文字
TrackItemTypeTitle TrackItemType = "title"
// 高级文字,一般用这个
TrackItemTypeAdvancedTitle TrackItemType = "advanced_title"
// 序列帧-文字(气泡文字)
TrackItemTypeSequenceTitle TrackItemType = "sequence_title"
// AE文件实现的文字动效文字
TrackItemTypePAGTitle TrackItemType = "pag_title"
// TODO
TrackItemTypeFrame TrackItemType = "frame"
// 字幕文字
TrackItemTypeSubtitle TrackItemType = "subtitle"
// TODO
TrackItemTypeImage TrackItemType = "image"
// 音频
TrackItemTypeAudio TrackItemType = "audio"
// 视频
TrackItemTypeVideo TrackItemType = "video"
// TODO: 转场
TrackItemTypeTransition TrackItemType = "transition"
)
type TrackItemBase struct {
Id string `json:"id"` // 根据uuid生成单个合成协议内唯一
Selection
Type TrackItemType `json:"type"`
AssetId string `json:"asset_id"` // cme素材id
}
// 剪辑时间线
type Selection struct {
StartTime int32 `json:"start_time"`
Duration int32 `json:"duration"`
}
// 元素片段
// 音频、视频、特效等元素必填
type Section struct {
From int32 `json:"from"`
To int32 `json:"to"`
}
type Position struct {
X int32 `json:"x"`
Y int32 `json:"y"`
}
type ItemSize struct {
Width int32 `json:"width,omitempty"`
Height int32 `json:"height,omitempty"`
}
type ItemContents struct {
Text string `json:"text"`
TextStyle *TextStyle `json:"text_style,omitempty"`
}
// 文字样式
type TextStyle struct {
// 通用
FontSize int32 `json:"font_size"`
FontColor string `json:"font_color"`
FontColorList []string `json:"font_color_list,omitempty"`
Font string `json:"font,omitempty"`
FontAssetId string `json:"font_asset_id,omitempty"`
FontAlpha int32 `json:"font_alpha,omitempty"`
FontBold int32 `json:"font_bold,omitempty"`
FontItalic int32 `json:"font_italic,omitempty"`
FontUnderline int32 `json:"font_uline,omitempty"`
FontAlign string `json:"font_align,omitempty"`
BackgroundColor string `json:"background_color,omitempty"`
BackgroundAlpha int32 `json:"background_alpha,omitempty"`
BorderWidth int32 `json:"border_width,omitempty"`
BorderColor string `json:"border_color,omitempty"`
Align string `json:"align,omitempty"`
// 内容填充文字可用
ShadowColor string `json:"shadow_color,omitempty"`
ShadowAngle int32 `json:"shadow_angle,omitempty"`
ShadowAlpha int32 `json:"shadow_alpha,omitempty"`
// 字幕可用
Height int32 `json:"height"`
Bold int32 `json:"bold,omitempty"`
Italic int32 `json:"italic,omitempty"`
BottomColor string `json:"bottom_color,omitempty"`
BottomAlpha int32 `json:"bottom_alpha,omitempty"`
MarginBottom int32 `json:"margin_bottom,omitempty"`
// 未知
// LetterSpacing int32 `json:"letter_spacing"`
// Leading int32 `json:"leading"`
// TextBox *TextBox `json:"text_box"`
}
type TextBox struct {
Width float32 `json:"width"`
Height float32 `json:"height"`
FontSize int `json:"font_size"`
}
func DefaultTextStyle() *TextStyle {
return &TextStyle{
FontSize: 60,
FontColor: "#FFFFFF",
Align: "center",
Height: 220,
Bold: 0,
Italic: 0,
}
}
func DefaultSubtitleStyle() *Style {
return &Style{
Id: uuid.NewString(),
TextStyle: &TextStyle{
FontSize: 30,
FontColor: "#FFFFFF",
Align: "center",
Height: 55,
Bold: 0,
Italic: 0,
},
}
}
func SubtitleStyleWithTextStyle(ts *TextStyle) *Style {
return &Style{
Id: uuid.NewString(),
TextStyle: ts,
}
}
// 轨道元素
type TrackItem struct {
TrackItemBase
Section *Section `json:"section,omitempty"`
ItemSize
Position *Position `json:"position,omitempty"`
TextStyleId string `json:"text_style_id,omitempty"`
Contents *ItemContents `json:"contents,omitempty"`
Operations []*Operation `json:"operations,omitempty"`
}
// 创建轨道
func NewTrackItem(trackItemType TrackItemType) *TrackItem {
return &TrackItem{
TrackItemBase: TrackItemBase{
Id: uuid.NewString(),
Type: trackItemType,
},
}
}
func (i *TrackItem) SetAssetId(assetId string) *TrackItem {
i.AssetId = assetId
return i
}
func (i *TrackItem) SetSelection(startTime, duration int32) *TrackItem {
i.Selection = Selection{StartTime: startTime, Duration: duration}
return i
}
func (i *TrackItem) SetSection(from, to int32) *TrackItem {
i.Section = &Section{From: from, To: to}
return i
}
func (i *TrackItem) SetItemSize(width, height int32) *TrackItem {
i.ItemSize = ItemSize{Width: width, Height: height}
return i
}
func (i *TrackItem) SetPosition(x, y int32) *TrackItem {
if i.Position != nil {
i.Position.X = x
i.Position.Y = y
} else {
i.Position = &Position{X: x, Y: y}
}
return i
}
func (i *TrackItem) SetSubtitle(text string) *TrackItem {
// i.TextStyleId = styleId
i.Contents = &ItemContents{Text: text}
return i
}
func (i *TrackItem) SetContents(text string, style *TextStyle) *TrackItem {
i.Contents = &ItemContents{Text: text, TextStyle: style}
return i
}
func (i *TrackItem) SetOperations(ops ...*Operation) *TrackItem {
i.Operations = append(i.Operations, ops...)
return i
}

115
ffcut/shelf/track_test.go Normal file
View File

@@ -0,0 +1,115 @@
package shelf_test
import (
"fmt"
"testing"
"github.com/fxkt-tech/liv/ffcut/shelf"
)
func TestExport(t *testing.T) {
trackData, err := shelf.New(
shelf.WithStageSize(540, 960),
).
AddTrack(
// 添加视频轨
shelf.NewTrack(shelf.TrackTypeVideo).
Append(
shelf.NewTrackItem(shelf.TrackItemTypeVideo).
SetAssetId("660521dc64b43b0001ce490a").
SetSelection(0, 5000).
SetSection(0, 5000).
SetItemSize(540, 960).
SetPosition(270, 480),
)).
AddTrack(
// 添加文字轨
shelf.NewTrack(shelf.TrackTypeTitle).
Append(
shelf.NewTrackItem(shelf.TrackItemTypeSequenceTitle).
SetAssetId("608bc4d689ea7200013ff242@Public@CME").
SetSelection(0, 5000).
SetPosition(270, 180).
SetContents("超好吃!", &shelf.TextStyle{
Font: "huakangshaonvwenziW5-2",
FontSize: 40,
FontColor: "#173563",
Align: "center",
}),
),
).
AddTrack(
// 添加音频轨
shelf.NewTrack(shelf.TrackTypeAudio).
Append(
shelf.NewTrackItem(shelf.TrackItemTypeAudio).
SetAssetId("65df048fb95d8900015302c9").
SetSelection(0, 5000).
SetSection(0, 5000),
),
).
AddTrack(
// 添加字幕轨
shelf.NewTrack(shelf.TrackTypeSubtitle).
Append(
shelf.NewTrackItem(shelf.TrackItemTypeSubtitle).
SetSelection(0, 1000).
SetSubtitle("此处字幕1"),
shelf.NewTrackItem(shelf.TrackItemTypeSubtitle).
SetSelection(1000, 3000).
SetSubtitle("此处字幕2"),
shelf.NewTrackItem(shelf.TrackItemTypeSubtitle).
SetSelection(4000, 1000).
SetSubtitle("此处字幕3"),
).
SetStyles(shelf.DefaultSubtitleStyle()),
).
Export() // 导出
if err != nil {
t.Fatal(err)
}
fmt.Println(trackData)
// t.Log(json.Pretty([]byte(trackData)))
}
func TestExec(t *testing.T) {
err := shelf.New(
// shelf.WithStageSize(540, 960),
shelf.WithStageSize(1920, 1080),
).
AppendTrack(
// 添加视频轨
shelf.NewTrack(shelf.TrackTypeVideo).
Append(
shelf.NewTrackItem(shelf.TrackItemTypeVideo).
SetAssetId(`in.mp4`).
SetSelection(0, 5000).
SetSection(0, 5000).
SetItemSize(540, 960).
SetPosition(600, 600),
),
shelf.NewTrack(shelf.TrackTypeVideo).
Append(
shelf.NewTrackItem(shelf.TrackItemTypeVideo).
SetAssetId(`in.mp4`).
SetSelection(8000, 2000).
SetSection(1000, 3000).
SetItemSize(540, 960).
SetPosition(0, 0),
shelf.NewTrackItem(shelf.TrackItemTypeVideo).
SetAssetId(`in.mp4`).
SetSelection(20000, 2000).
SetSection(3000, 5000).
SetItemSize(540, 960).
SetPosition(100, 50),
),
).
Exec() // 导出
if err != nil {
t.Fatal(err)
}
// fmt.Println(trackData)
// t.Log(json.Pretty([]byte(trackData)))
}

View File

@@ -25,7 +25,7 @@ func Overlay[T Expr](dx, dy T) *SingleFilter {
}
}
// Deprecated: 一个图像覆盖另一个图像(可激活某一时间段)
// 一个图像覆盖另一个图像(可激活某一时间段)
func OverlayWithEnable[T Expr](dx, dy T, enable string) *SingleFilter {
return &SingleFilter{
name: naming.Default.Gen(),
@@ -33,6 +33,13 @@ func OverlayWithEnable[T Expr](dx, dy T, enable string) *SingleFilter {
}
}
func OverlayWithTime[T Expr](dx, dy T, t float32) *SingleFilter {
return &SingleFilter{
name: naming.Default.Gen(),
content: fmt.Sprintf("overlay=%v:%v:t=%f", dx, dy, t),
}
}
// 缩放
func Scale[T Expr](w, h T) *SingleFilter {
var ww, hh any = w, h
@@ -99,7 +106,7 @@ func SetPTS(expr string) *SingleFilter {
}
// 截取某一时间段
func Trim(s, e float64) *SingleFilter {
func Trim(s, e float32) *SingleFilter {
var ps []string
if s != 0 {
ps = append(ps, fmt.Sprintf("start=%f", s))

View File

@@ -14,12 +14,12 @@ type Input struct {
cv string
r string
safe string
i string // i is input file.
ss float64 // ss is start_time.
t float64 // t is duration.
ss float32 // ss is start_time.
t float32 // t is duration.
itsoffset float32 // offset
metadata []string // kv pair.
f string // format
// ext []string // extra params.
i string // i is input file.
}
func New(opts ...Option) *Input {
@@ -42,7 +42,7 @@ func WithMetadata(i string, kvs []string) *Input {
return &Input{i: i, metadata: kvs}
}
func WithTime(ss, t float64, i string) *Input {
func WithTime(ss, t float32, i string) *Input {
return &Input{
ss: ss,
t: t,
@@ -82,6 +82,9 @@ func (i *Input) Params() (params []string) {
if i.t != 0 {
params = append(params, "-t", fmt.Sprintf("%.6f", i.t))
}
if i.itsoffset != 0 {
params = append(params, "-itsoffset", fmt.Sprintf("%.6f", i.itsoffset))
}
if i.f != "" {
params = append(params, "-f", i.f)
}

View File

@@ -14,13 +14,13 @@ func VideoCodec(cv string) Option {
}
}
func StartTime(ss float64) Option {
func StartTime(ss float32) Option {
return func(o *Input) {
o.ss = ss
}
}
func Duration(t float64) Option {
func Duration(t float32) Option {
return func(o *Input) {
o.t = t
}

View File

@@ -82,13 +82,13 @@ func Metadata(k, v string) Option {
}
}
func StartTime(ss float64) Option {
func StartTime(ss float32) Option {
return func(o *Output) {
o.ss = ss
}
}
func Duration(t float64) Option {
func Duration(t float32) Option {
return func(o *Output) {
o.t = t
}

View File

@@ -1,6 +1,7 @@
package output
import (
"fmt"
"strconv"
"strings"
@@ -20,8 +21,8 @@ type Output struct {
threads int32 // thread counts, default 4.
max_muxing_queue_size int32 // queue size when muxing, default 4086.
movflags string // location of mp4 moov.
ss float64 // ss is start_time.
t float64 // t is duration.
ss float32 // ss is start_time.
t float32 // t is duration.
f string // f is -f format.
file string
var_stream_map string
@@ -151,10 +152,10 @@ func (o *Output) Params() (params []string) {
params = append(params, "-movflags", o.movflags)
}
if o.ss != 0 {
params = append(params, "-ss", strconv.FormatFloat(o.ss, 'f', -1, 64))
params = append(params, "-ss", fmt.Sprintf("%f", o.ss))
}
if o.t != 0 {
params = append(params, "-t", strconv.FormatFloat(o.t, 'f', -1, 64))
params = append(params, "-t", fmt.Sprintf("%f", o.t))
}
if o.f != "" {
params = append(params, "-f", o.f)

View File

@@ -12,7 +12,7 @@ type ProbeStream struct {
CodecName string `json:"codec_name,omitempty"`
Profile string `json:"profile,omitempty"`
BitRate int32 `json:"bit_rate,omitempty,string"`
Duration float64 `json:"duration,omitempty,string"`
Duration float32 `json:"duration,omitempty,string"`
// video
Width int32 `json:"width,omitempty"`
@@ -34,5 +34,5 @@ type ProbeStream struct {
type ProbeFormat struct {
FormatName string `json:"format_name,omitempty"`
Size int64 `json:"size,omitempty,string"`
Duration float64 `json:"duration,omitempty,string"`
Duration float32 `json:"duration,omitempty,string"`
}

1
go.mod
View File

@@ -4,5 +4,6 @@ go 1.22
require (
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
github.com/google/uuid v1.6.0
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f
)

2
go.sum
View File

@@ -3,6 +3,8 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

5
internal/conv/conv.go Normal file
View File

@@ -0,0 +1,5 @@
package conv
func MillToF32(mill int32) float32 {
return float32(mill) / 1000
}

View File

@@ -0,0 +1,35 @@
package json
import (
"bytes"
"encoding/json"
)
func ToString(i any) string {
if i == nil {
return ""
}
b, _ := json.Marshal(i)
return string(b)
}
func ToBytes(i any) []byte {
if i == nil {
return nil
}
b, _ := json.Marshal(i)
return b
}
// json字节流转struct/map等不会返回错误所以确保传入的内容一定是能解析的
func ToV[T any](b []byte) T {
var i T
json.Unmarshal(b, &i)
return i
}
func Pretty(b []byte) string {
var str bytes.Buffer
_ = json.Indent(&str, b, "", " ")
return str.String()
}

View File

@@ -0,0 +1,24 @@
package json_test
import (
"fmt"
"testing"
"github.com/fxkt-tech/liv/internal/encoding/json"
)
func TestPretty(t *testing.T) {
x := []byte(`{"x":1}`)
fmt.Println(json.Pretty(x))
}
type Person struct {
Name string `json:"name,omitempty"`
Age int `json:"age,omitempty"`
}
func TestToObjectT(t *testing.T) {
b := []byte(`{"name":"xx","age":1}`)
x := json.ToV[*Person](b)
fmt.Println(x.Name, x.Age)
}

View File

@@ -90,7 +90,7 @@ func (ss *Snapshot) Simple(ctx context.Context, params *SnapshotParams) error {
func (ss *Snapshot) Sprite(ctx context.Context, params *SpriteParams) error {
var (
duration = float64(params.XLen*params.YLen) * params.Interval
duration = float32(params.XLen*params.YLen) * params.Interval
frames = params.XLen * params.YLen
)
@@ -179,14 +179,14 @@ func (ss *Snapshot) SVGMark(ctx context.Context, params *SVGMarkParams) error {
for _, annotation := range params.Annotations {
switch annotation.Type {
case "rect":
fromx := int(annotation.FromPoint.X * float64(vstream.Width))
fromy := int(annotation.FromPoint.Y * float64(vstream.Height))
tox := int(annotation.ToPoint.X * float64(vstream.Width))
toy := int(annotation.ToPoint.Y * float64(vstream.Height))
minx := int(min(float64(fromx), float64(tox)))
miny := int(min(float64(fromy), float64(toy)))
w := int(math.Abs(float64(fromx - tox)))
h := int(math.Abs(float64(fromy - toy)))
fromx := int(annotation.FromPoint.X * float32(vstream.Width))
fromy := int(annotation.FromPoint.Y * float32(vstream.Height))
tox := int(annotation.ToPoint.X * float32(vstream.Width))
toy := int(annotation.ToPoint.Y * float32(vstream.Height))
minx := int(min(float32(fromx), float32(tox)))
miny := int(min(float32(fromy), float32(toy)))
w := int(math.Abs(float32(fromx - tox)))
h := int(math.Abs(float32(fromy - toy)))
styles := []string{"fill:transparent"}
if annotation.Stroke != "" {
styles = append(styles, fmt.Sprintf("stroke:%s", annotation.Stroke))
@@ -199,8 +199,8 @@ func (ss *Snapshot) SVGMark(ctx context.Context, params *SVGMarkParams) error {
var d string
plen := len(annotation.Points)
for i, point := range annotation.Points {
x := int(point.X * float64(vstream.Width))
y := int(point.Y * float64(vstream.Height))
x := int(point.X * float32(vstream.Width))
y := int(point.Y * float32(vstream.Height))
if i == 0 {
d = fmt.Sprintf("%sM%d %d ", d, x, y)
} else if i == plen-1 {
@@ -232,10 +232,10 @@ func (ss *Snapshot) SVGMark(ctx context.Context, params *SVGMarkParams) error {
for i, point := range unitPoints {
orix := point.X
oriy := point.Y
fromx := annotation.FromPoint.X * float64(vstream.Width)
fromy := annotation.FromPoint.Y * float64(vstream.Height)
tox := annotation.ToPoint.X * float64(vstream.Width)
toy := annotation.ToPoint.Y * float64(vstream.Height)
fromx := annotation.FromPoint.X * float32(vstream.Width)
fromy := annotation.FromPoint.Y * float32(vstream.Height)
tox := annotation.ToPoint.X * float32(vstream.Width)
toy := annotation.ToPoint.Y * float32(vstream.Height)
// 根据变换矩阵,变换后的点坐标(A, B)为
// A = a(x2 - x1) - b(y2 - y1) + x1
// B = a(y2 - y1) + b(x2 - x1) + y1
@@ -259,8 +259,8 @@ func (ss *Snapshot) SVGMark(ctx context.Context, params *SVGMarkParams) error {
canvas.Path(d, strings.Join(styles, ";"))
case "text":
fromx := int(annotation.FromPoint.X * float64(vstream.Width))
fromy := int(annotation.FromPoint.Y * float64(vstream.Height))
fromx := int(annotation.FromPoint.X * float32(vstream.Width))
fromy := int(annotation.FromPoint.Y * float32(vstream.Height))
var styles []string
if annotation.Stroke != "" {
styles = append(styles, fmt.Sprintf("fill:%s", annotation.Stroke))

View File

@@ -3,7 +3,7 @@ package liv
type SnapshotParams struct {
Infile string
Outfile string
StartTime float64
StartTime float32
Interval int32
Num int32
FrameType int32 // 0-仅关键帧 1-指定时间点的帧
@@ -18,13 +18,13 @@ type SpriteParams struct {
YLen int32
Width int32
Height int32
Interval float64
Interval float32
}
type SVGMarkParams struct {
Infile string
Outfile string
StartTime float64 `json:"start_time,omitempty"`
StartTime float32 `json:"start_time,omitempty"`
Annotations []*SVGAnnotation `json:"annotation,omitempty"`
}
@@ -40,6 +40,6 @@ type SVGAnnotation struct {
}
type Point struct {
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
X float32 `json:"x,omitempty"`
Y float32 `json:"y,omitempty"`
}

View File

@@ -34,7 +34,7 @@ type ConcatParams struct {
Infiles []string
ConcatFile string // eg. mylist.txt
Outfile string
Duration float64
Duration float32
}
type ExtractAudioParams struct {
@@ -89,10 +89,10 @@ type Audio struct {
type Logo struct {
File string `json:"file,omitempty"`
Pos string `json:"pos,omitempty"`
Dx float64 `json:"dx,omitempty"`
Dy float64 `json:"dy,omitempty"`
LW float64 `json:"lw,omitempty"`
LH float64 `json:"lh,omitempty"`
Dx float32 `json:"dx,omitempty"`
Dy float32 `json:"dy,omitempty"`
LW float32 `json:"lw,omitempty"`
LH float32 `json:"lh,omitempty"`
}
func (l *Logo) NeedScale() bool {
@@ -101,20 +101,20 @@ func (l *Logo) NeedScale() bool {
// 矩形框
type Rectangle struct {
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
W float64 `json:"w,omitempty"`
H float64 `json:"h,omitempty"`
X float32 `json:"x,omitempty"`
Y float32 `json:"y,omitempty"`
W float32 `json:"w,omitempty"`
H float32 `json:"h,omitempty"`
}
type Delogo struct {
SS float64 `json:"ss,omitempty"`
SS float32 `json:"ss,omitempty"`
Rect *Rectangle `json:"rect,omitempty"`
}
type Clip struct {
Seek float64 `json:"seek,omitempty"`
Duration float64 `json:"duration,omitempty"`
Seek float32 `json:"seek,omitempty"`
Duration float32 `json:"duration,omitempty"`
}
type HLS struct {