update: add webm support
This commit is contained in:
parent
84c816cb8f
commit
1db6864ea0
10
README.md
10
README.md
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue