From e74e94a3a6efc22749602f3b1e67b84dbbebcc03 Mon Sep 17 00:00:00 2001 From: Justyer <1554694323@qq.com> Date: Sun, 7 Apr 2024 03:08:42 +0800 Subject: [PATCH] feat(ffcut): video track --- ffcut/shelf/errors.go | 10 ++ ffcut/shelf/operation.go | 38 ++++ ffcut/shelf/options.go | 9 + ffcut/shelf/track.go | 270 ++++++++++++++++++++++++++++ ffcut/shelf/track_item.go | 208 +++++++++++++++++++++ ffcut/shelf/track_test.go | 115 ++++++++++++ ffmpeg/filter/video.go | 11 +- ffmpeg/input/input.go | 23 +-- ffmpeg/input/options.go | 4 +- ffmpeg/output/options.go | 4 +- ffmpeg/output/outout.go | 9 +- ffprobe/model.go | 4 +- go.mod | 1 + go.sum | 2 + internal/conv/conv.go | 5 + internal/encoding/json/json.go | 35 ++++ internal/encoding/json/json_test.go | 24 +++ snapshot.go | 34 ++-- snapshot_params.go | 10 +- transcode_params.go | 24 +-- 20 files changed, 784 insertions(+), 56 deletions(-) create mode 100644 ffcut/shelf/errors.go create mode 100644 ffcut/shelf/operation.go create mode 100644 ffcut/shelf/options.go create mode 100644 ffcut/shelf/track.go create mode 100644 ffcut/shelf/track_item.go create mode 100644 ffcut/shelf/track_test.go create mode 100644 internal/conv/conv.go create mode 100644 internal/encoding/json/json.go create mode 100644 internal/encoding/json/json_test.go diff --git a/ffcut/shelf/errors.go b/ffcut/shelf/errors.go new file mode 100644 index 0000000..630785a --- /dev/null +++ b/ffcut/shelf/errors.go @@ -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") +) diff --git a/ffcut/shelf/operation.go b/ffcut/shelf/operation.go new file mode 100644 index 0000000..b0801a8 --- /dev/null +++ b/ffcut/shelf/operation.go @@ -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, + }, + } +} diff --git a/ffcut/shelf/options.go b/ffcut/shelf/options.go new file mode 100644 index 0000000..c522748 --- /dev/null +++ b/ffcut/shelf/options.go @@ -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 + } +} diff --git a/ffcut/shelf/track.go b/ffcut/shelf/track.go new file mode 100644 index 0000000..4ba9edc --- /dev/null +++ b/ffcut/shelf/track.go @@ -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 +} diff --git a/ffcut/shelf/track_item.go b/ffcut/shelf/track_item.go new file mode 100644 index 0000000..407bf49 --- /dev/null +++ b/ffcut/shelf/track_item.go @@ -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 +} diff --git a/ffcut/shelf/track_test.go b/ffcut/shelf/track_test.go new file mode 100644 index 0000000..baa7855 --- /dev/null +++ b/ffcut/shelf/track_test.go @@ -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))) +} diff --git a/ffmpeg/filter/video.go b/ffmpeg/filter/video.go index 92b7aea..1d86765 100644 --- a/ffmpeg/filter/video.go +++ b/ffmpeg/filter/video.go @@ -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)) diff --git a/ffmpeg/input/input.go b/ffmpeg/input/input.go index ba6c1e1..989e3ae 100644 --- a/ffmpeg/input/input.go +++ b/ffmpeg/input/input.go @@ -11,15 +11,15 @@ import ( type Input struct { idx int - cv string - r string - safe string - i string // i is input file. - ss float64 // ss is start_time. - t float64 // t is duration. - metadata []string // kv pair. - f string // format - // ext []string // extra params. + cv string + r string + safe string + ss float32 // ss is start_time. + t float32 // t is duration. + itsoffset float32 // offset + metadata []string // kv pair. + f string // format + 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) } diff --git a/ffmpeg/input/options.go b/ffmpeg/input/options.go index ec919fe..a840069 100644 --- a/ffmpeg/input/options.go +++ b/ffmpeg/input/options.go @@ -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 } diff --git a/ffmpeg/output/options.go b/ffmpeg/output/options.go index 34bfec2..b4330a2 100644 --- a/ffmpeg/output/options.go +++ b/ffmpeg/output/options.go @@ -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 } diff --git a/ffmpeg/output/outout.go b/ffmpeg/output/outout.go index 556da61..f56af84 100644 --- a/ffmpeg/output/outout.go +++ b/ffmpeg/output/outout.go @@ -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) diff --git a/ffprobe/model.go b/ffprobe/model.go index eae6da1..5d53c13 100644 --- a/ffprobe/model.go +++ b/ffprobe/model.go @@ -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"` } diff --git a/go.mod b/go.mod index 9010ef3..ce5d302 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index a960f90..412fc24 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/conv/conv.go b/internal/conv/conv.go new file mode 100644 index 0000000..778f6c0 --- /dev/null +++ b/internal/conv/conv.go @@ -0,0 +1,5 @@ +package conv + +func MillToF32(mill int32) float32 { + return float32(mill) / 1000 +} diff --git a/internal/encoding/json/json.go b/internal/encoding/json/json.go new file mode 100644 index 0000000..fa9bb33 --- /dev/null +++ b/internal/encoding/json/json.go @@ -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() +} diff --git a/internal/encoding/json/json_test.go b/internal/encoding/json/json_test.go new file mode 100644 index 0000000..e7301dc --- /dev/null +++ b/internal/encoding/json/json_test.go @@ -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) +} diff --git a/snapshot.go b/snapshot.go index 7065fbb..d2dfc18 100644 --- a/snapshot.go +++ b/snapshot.go @@ -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)) diff --git a/snapshot_params.go b/snapshot_params.go index 3622977..a017261 100644 --- a/snapshot_params.go +++ b/snapshot_params.go @@ -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"` } diff --git a/transcode_params.go b/transcode_params.go index 8fd64ea..7e2cc5f 100644 --- a/transcode_params.go +++ b/transcode_params.go @@ -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 {