From 5112a16827bcb2751014099ab91ac5693af0af8f Mon Sep 17 00:00:00 2001 From: eugene Date: Sat, 6 Sep 2025 23:56:06 +0300 Subject: [PATCH] webm: basic logic --- README.md | 3 + cmd/telegram_bot/telegram_bot.go | 12 -- cmd/tgs_file_convert/tgs_file_convert.go | 3 +- internal/converter/tgs.go | 6 +- internal/converter/webm.go | 19 +++ internal/tgs/service.go | 10 +- internal/webm/service.go | 169 +++++++++++++++++++++++ 7 files changed, 201 insertions(+), 21 deletions(-) delete mode 100644 cmd/telegram_bot/telegram_bot.go create mode 100644 internal/converter/webm.go create mode 100644 internal/webm/service.go diff --git a/README.md b/README.md index 67e9177..bece62c 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,6 @@ ### WEBM - WEBM → MP4 **[WIP]** - WEBM → GIF **[WIP]** +- WEBM → PNG (first framge, all frames, N frame) **[WIP]** +- WEBM → JPEG (first framge, all frames, N frame) **[WIP]** +- WEBM → WEBP (first framge, all frames, N frame) **[WIP]** \ No newline at end of file diff --git a/cmd/telegram_bot/telegram_bot.go b/cmd/telegram_bot/telegram_bot.go deleted file mode 100644 index f8224f6..0000000 --- a/cmd/telegram_bot/telegram_bot.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "os" - - "github.com/yazmeyaa/telegram_sticker_converter/internal/logger" -) - -func main() { - logger := logger.NewLogger(os.Stdout, "telegram_bot") - logger.Info("Starting telegram bot") -} diff --git a/cmd/tgs_file_convert/tgs_file_convert.go b/cmd/tgs_file_convert/tgs_file_convert.go index 9dce41f..66b6d84 100644 --- a/cmd/tgs_file_convert/tgs_file_convert.go +++ b/cmd/tgs_file_convert/tgs_file_convert.go @@ -58,7 +58,7 @@ func main() { os.Exit(1) } - opts := converter.TransformOptions{ + opts := converter.TGSTransformOptions{ Format: converter.OutputFormat(*format), Frame: frameSel, FrameIndex: *frameIndex, @@ -66,7 +66,6 @@ func main() { ResizeWidth: *resizeW, ResizeHeight: *resizeH, } - fmt.Fprintf(os.Stdout, "opts: %+v\n", opts) service := converter.TGSConverterService(tgs.NewService()) if err := service.Transform(context.Background(), in, out, opts); err != nil { diff --git a/internal/converter/tgs.go b/internal/converter/tgs.go index 7049be7..b6ed622 100644 --- a/internal/converter/tgs.go +++ b/internal/converter/tgs.go @@ -13,6 +13,7 @@ const ( FrameFirst FrameSelector = iota FrameAll FrameN + FrameRange ) var ( @@ -29,15 +30,16 @@ const ( FormatLottie OutputFormat = "lottie" ) -type TransformOptions struct { +type TGSTransformOptions struct { Format OutputFormat Frame FrameSelector FrameIndex int + FrameOffset int Qualtity int ResizeWidth int ResizeHeight int } type TGSConverterService interface { - Transform(ctx context.Context, data io.Reader, out io.Writer, opts TransformOptions) error + Transform(ctx context.Context, data io.Reader, out io.Writer, opts TGSTransformOptions) error } diff --git a/internal/converter/webm.go b/internal/converter/webm.go new file mode 100644 index 0000000..c312658 --- /dev/null +++ b/internal/converter/webm.go @@ -0,0 +1,19 @@ +package converter + +import ( + "context" + "io" +) + +type WEBMTransformOptions struct { + Format OutputFormat + Width uint + Height uint + Frame FrameSelector + FrameIndex uint + FrameOffset uint +} + +type WEBMConverter interface { + Transform(ctx context.Context, in io.Reader, out io.Writer, opts WEBMTransformOptions) error +} diff --git a/internal/tgs/service.go b/internal/tgs/service.go index a657721..6164488 100644 --- a/internal/tgs/service.go +++ b/internal/tgs/service.go @@ -17,7 +17,7 @@ import ( "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" ) -func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.TransformOptions) error { +func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.TGSTransformOptions) error { data, err := io.ReadAll(in) if err != nil { return err @@ -57,7 +57,7 @@ func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writ return converter.ErrUnknownFormat } -func (t tgsServiceImpl) processFrames(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TransformOptions) error { +func (t tgsServiceImpl) processFrames(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TGSTransformOptions) error { width := uint(opts.ResizeWidth) height := uint(opts.ResizeHeight) @@ -111,7 +111,7 @@ var ( } ) -func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TransformOptions) error { +func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TGSTransformOptions) error { totalFrames := rlottie.LottieAnimationGetTotalframe(anim) frameRate := rlottie.LottieAnimationGetFramerate(anim) var preset ffmpeg_go.KwArgs @@ -168,7 +168,7 @@ func BGRAtoRGBA(buf []byte) { } } -func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, out io.Writer, opts converter.TransformOptions) error { +func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, out io.Writer, opts converter.TGSTransformOptions) error { BGRAtoRGBA(frameBuffer) img := &image.RGBA{ Pix: frameBuffer, @@ -190,7 +190,7 @@ func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, return converter.ErrUnknownFormat } -func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_Animation, frameBuffer []byte, out io.Writer, opts converter.TransformOptions) error { +func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_Animation, frameBuffer []byte, out io.Writer, opts converter.TGSTransformOptions) error { totalFrames := rlottie.LottieAnimationGetTotalframe(anim) archive := zip.NewWriter(out) defer archive.Close() diff --git a/internal/webm/service.go b/internal/webm/service.go new file mode 100644 index 0000000..7f2af58 --- /dev/null +++ b/internal/webm/service.go @@ -0,0 +1,169 @@ +package webm + +import ( + "context" + "fmt" + "io" + + ffmpeg_go "github.com/u2takey/ffmpeg-go" + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" +) + +type webmService struct{} + +func NewService() *webmService { + return &webmService{} +} + +func (ws webmService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { + return ws.process(ctx, in, out, opts) +} + +var ( + PresetMP4 = ffmpeg_go.KwArgs{ + "vcodec": "libx264", + "format": "mp4", + "pix_fmt": "yuv420p", + "movflags": "frag_keyframe+empty_moov", + "preset": "ultrafast", + "tune": "zerolatency", + } + + PresetPNG = ffmpeg_go.KwArgs{ + "f": "image2pipe", + "c:v": "png", + "vsync": "0", + } + + PresetJPEG = ffmpeg_go.KwArgs{ + "f": "image2pipe", + "c:v": "mjpeg", + "vsync": "0", + } + + PresetWEBP = ffmpeg_go.KwArgs{ + "f": "image2pipe", + "c:v": "libwebp", + } + PresetGIF = ffmpeg_go.KwArgs{"f": "gif"} +) + +func buildPreset(opts converter.WEBMTransformOptions) (ffmpeg_go.KwArgs, error) { + switch opts.Format { + case converter.FormatGIF: + return PresetGIF, nil + case converter.FormatMP4: + return PresetMP4, nil + case converter.FormatPNG: + switch opts.Frame { + case converter.FrameAll: + return PresetPNG, nil + case converter.FrameFirst: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetPNG, + { + "vframes": "1", + }, + }), nil + case converter.FrameN: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetPNG, + { + "vframes": "1", + "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), + }, + }), nil + case converter.FrameRange: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetPNG, + { + "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), + }, + }), nil + } + case converter.FormatJPEG: + switch opts.Frame { + case converter.FrameAll: + return PresetJPEG, nil + case converter.FrameFirst: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetJPEG, + { + "frames": "1", + }, + }), nil + case converter.FrameN: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetJPEG, + { + "vframes": "1", + "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), + }, + }), nil + case converter.FrameRange: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetJPEG, + { + "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), + }, + }), nil + } + case converter.FormatWEBP: + switch opts.Frame { + case converter.FrameAll: + return PresetWEBP, nil + case converter.FrameFirst: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetWEBP, + { + "frames": "1", + }, + }), nil + case converter.FrameN: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetWEBP, + { + "vframes": "1", + "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), + }, + }), nil + case converter.FrameRange: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetWEBP, + { + "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), + }, + }), nil + } + } + return ffmpeg_go.KwArgs{}, converter.ErrUnknownFormat +} + +func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { + r, w := io.Pipe() + + go func() { + defer w.Close() + io.Copy(w, in) + }() + + preset, err := buildPreset(opts) + if err != nil { + return err + } + + err = ffmpeg_go. + Input("pipe:0", ffmpeg_go.KwArgs{ + "f": "webm", + }). + Output("pipe:1", preset). + WithInput(r). + WithOutput(out). + Run() + + if err != nil { + return err + } + + return nil +}