update: add webm support

This commit is contained in:
eugene 2025-09-07 22:57:44 +03:00
parent 84c816cb8f
commit 1db6864ea0
5 changed files with 242 additions and 13 deletions

View File

@ -20,8 +20,8 @@
- WEBP → JPEG **[WIP]** - WEBP → JPEG **[WIP]**
### WEBM ### WEBM
- WEBM → MP4 **[WIP]** - WEBM → MP4
- WEBM → GIF **[WIP]** - WEBM → GIF
- WEBM → PNG (first framge, all frames, N frame, frames range) **[WIP]** - WEBM → PNG (first framge, all frames, N frame, frames range)
- WEBM → JPEG (first framge, all frames, N frame, frames range) **[WIP]** - WEBM → JPEG (first framge, all frames, N frame, frames range)
- WEBM → WEBP (first framge, all frames, N frame, frames range) **[WIP]** - WEBM → WEBP (first framge, all frames, N frame, frames range)

View File

@ -0,0 +1,75 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"github.com/yazmeyaa/telegram_sticker_converter/internal/converter"
"github.com/yazmeyaa/telegram_sticker_converter/internal/webm"
)
func main() {
filePath := flag.String("file", "", "path to input .tgs file")
outPath := flag.String("out", "", "path to output file")
format := flag.String("format", "png", "output format (png|jpeg|webp|gif|webm|mp4|lottie)")
frame := flag.String("frame", "all", "frame selector (first|all|n)")
frameIndex := flag.Uint("frame-index", 0, "frame index (used only with frame=n)")
resizeW := flag.Uint("resize-width", 0, "resize width (0 = keep original)")
resizeH := flag.Uint("resize-height", 0, "resize height (0 = keep original)")
flag.Parse()
if *filePath == "" {
fmt.Fprintln(os.Stderr, "error: -file is required")
os.Exit(1)
}
if *outPath == "" {
fmt.Fprintln(os.Stderr, "error: -out is required")
os.Exit(1)
}
in, err := os.Open(*filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening file: %v\n", err)
os.Exit(1)
}
defer in.Close()
out, err := os.Create(*outPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating output: %v\n", err)
os.Exit(1)
}
defer out.Close()
var frameSel converter.FrameSelector
switch *frame {
case "first":
frameSel = converter.FrameFirst
case "all":
frameSel = converter.FrameAll
case "n":
frameSel = converter.FrameN
default:
fmt.Fprintf(os.Stderr, "unknown frame selector: %s\n", *frame)
os.Exit(1)
}
opts := converter.WEBMTransformOptions{
Format: converter.OutputFormat(*format),
Frame: frameSel,
FrameIndex: *frameIndex,
Width: *resizeW,
Height: *resizeH,
}
service := converter.WEBMConverter(webm.NewService())
if err := service.Transform(context.Background(), in, out, opts); err != nil {
fmt.Fprintf(os.Stderr, "transform failed: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}

View File

@ -150,6 +150,7 @@ func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_An
"s": fmt.Sprintf("%dx%d", opts.ResizeWidth, opts.ResizeHeight), "s": fmt.Sprintf("%dx%d", opts.ResizeWidth, opts.ResizeHeight),
"r": frameRate, "r": frameRate,
}). }).
Silent(true).
Output("pipe:1", preset). Output("pipe:1", preset).
WithInput(r). WithInput(r).
WithOutput(out). WithOutput(out).

View File

@ -0,0 +1,111 @@
package webm
import (
"bytes"
"errors"
"io"
"github.com/yazmeyaa/telegram_sticker_converter/internal/converter"
)
type frameScanner struct {
r io.Reader
opts converter.WEBMTransformOptions
buf []byte
frame []byte
eof bool
}
func newScanner(r io.Reader, opts converter.WEBMTransformOptions) *frameScanner {
return &frameScanner{
r: r,
opts: opts,
buf: make([]byte, 0, 64*1024),
}
}
var (
pngStart = []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
pngEnd = []byte{0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82}
jpegStart = []byte{0xFF, 0xD8}
jpegEnd = []byte{0xFF, 0xD9}
webpStart = []byte{'R', 'I', 'F', 'F'}
webpWebP = []byte{'W', 'E', 'B', 'P'}
)
func (f *frameScanner) signatures() ([]byte, []byte) {
switch f.opts.Format {
case converter.FormatPNG:
return pngStart, pngEnd
case converter.FormatJPEG:
return jpegStart, jpegEnd
case converter.FormatWEBP:
return webpStart, nil
default:
return nil, nil
}
}
func (f *frameScanner) Next() ([]byte, error) {
if f.eof {
return nil, io.EOF
}
startSig, endSig := f.signatures()
if startSig == nil {
return nil, converter.ErrUnknownFormat
}
for {
startIdx := bytes.Index(f.buf, startSig)
if startIdx >= 0 {
switch f.opts.Format {
case converter.FormatWEBP:
if len(f.buf[startIdx:]) < 12 {
break
}
if !bytes.Equal(f.buf[startIdx+8:startIdx+12], webpWebP) {
f.buf = f.buf[startIdx+4:]
continue
}
size := int(uint32(f.buf[startIdx+4]) |
uint32(f.buf[startIdx+5])<<8 |
uint32(f.buf[startIdx+6])<<16 |
uint32(f.buf[startIdx+7])<<24)
total := 8 + size
if len(f.buf[startIdx:]) < total {
break
}
frame := f.buf[startIdx : startIdx+total]
f.buf = append([]byte{}, f.buf[startIdx+total:]...)
return frame, nil
default:
endIdx := bytes.Index(f.buf[startIdx+len(startSig):], endSig)
if endIdx >= 0 {
endIdx += startIdx + len(startSig)
frame := f.buf[startIdx : endIdx+len(endSig)]
f.buf = append([]byte{}, f.buf[endIdx+len(endSig):]...)
return frame, nil
}
}
}
tmp := make([]byte, 8192)
n, err := f.r.Read(tmp)
if n > 0 {
f.buf = append(f.buf, tmp[:n]...)
continue
}
if errors.Is(err, io.EOF) {
f.eof = true
return nil, io.EOF
}
if err != nil {
return nil, err
}
}
}

View File

@ -1,7 +1,9 @@
package webm package webm
import ( import (
"archive/zip"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
@ -136,15 +138,17 @@ func buildPreset(opts converter.WEBMTransformOptions) (ffmpeg_go.KwArgs, error)
}), nil }), nil
} }
} }
return ffmpeg_go.KwArgs{}, converter.ErrUnknownFormat return ffmpeg_go.KwArgs{}, converter.ErrUnknownFormat
} }
func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error {
r, w := io.Pipe() rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe()
go func() { go func() {
defer w.Close() defer wIn.Close()
io.Copy(w, in) _, _ = io.Copy(wIn, in)
}() }()
preset, err := buildPreset(opts) preset, err := buildPreset(opts)
@ -152,16 +156,54 @@ func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer,
return err return err
} }
err = ffmpeg_go. stream := ffmpeg_go.
Input("pipe:0", ffmpeg_go.KwArgs{ Input("pipe:0", ffmpeg_go.KwArgs{
"f": "webm", "f": "webm",
}). }).
Silent(true).
Output("pipe:1", preset). Output("pipe:1", preset).
WithInput(r). WithInput(rIn).
WithOutput(out). WithOutput(wOut)
Run()
if err != nil { if (opts.Frame == converter.FrameAll || opts.Frame == converter.FrameRange) &&
(opts.Format == converter.FormatWEBP || opts.Format == converter.FormatJPEG || opts.Format == converter.FormatPNG) {
go func() {
defer wOut.Close()
_ = stream.Run()
}()
zw := zip.NewWriter(out)
defer zw.Close()
frameIdx := 0
scanner := newScanner(rOut, opts)
for {
frame, err := scanner.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
f, err := zw.Create(fmt.Sprintf("frame_%d.%s", frameIdx, string(opts.Format)))
if err != nil {
return err
}
if _, err := f.Write(frame); err != nil {
return err
}
frameIdx++
}
return nil
}
defer wOut.Close()
if err := stream.WithOutput(out).Run(); err != nil {
return err return err
} }