From 74b92c2d78ded80923fa4ba18cd671c060bef9a5 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 10 Feb 2026 13:30:09 +0300 Subject: [PATCH 1/4] chore(.gitignore): update ignored files list - add CLAUDE.md to the ignored files for better cleanliness --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 331ef53d2..ea93751e0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ website/.vitepress/cache website/.vitepress/dist node_modules -package-lock.json \ No newline at end of file +package-lock.json + +*/**/CLAUDE.md From ea03aa832da7ff4a071c9d9a58571aa171aafbf3 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 10 Mar 2026 21:37:29 +0300 Subject: [PATCH 2/4] feat(webp): add WebP streaming and snapshot APIs - implement WebP streaming with multipart support - add snapshot endpoint for WebP format with quality options - introduce WebP encoding using pure Go library without FFmpeg - update documentation and links for new WebP features --- go.mod | 3 +- go.sum | 2 + internal/webp/webp.go | 160 ++++++++++++++++++++++++++++++++++++ internal/webp/webp_test.go | 16 ++++ main.go | 2 + pkg/image/producer.go | 35 ++++++++ pkg/webp/consumer.go | 61 ++++++++++++++ pkg/webp/helpers.go | 84 +++++++++++++++++++ pkg/webp/rtp.go | 11 +++ pkg/webp/webp_test.go | 163 +++++++++++++++++++++++++++++++++++++ pkg/webp/writer.go | 38 +++++++++ website/api/openapi.yaml | 61 ++++++++++++++ www/links.html | 2 + www/schema.json | 4 + 14 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 internal/webp/webp.go create mode 100644 internal/webp/webp_test.go create mode 100644 pkg/webp/consumer.go create mode 100644 pkg/webp/helpers.go create mode 100644 pkg/webp/rtp.go create mode 100644 pkg/webp/webp_test.go create mode 100644 pkg/webp/writer.go diff --git a/go.mod b/go.mod index 485509e68..0f2354f23 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/AlexxIT/go2rtc -go 1.24.0 +go 1.26.1 require ( github.com/asticode/go-astits v1.14.0 @@ -43,6 +43,7 @@ require ( github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/turn/v4 v4.1.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/skrashevich/go-webp v0.0.0-20260309074808-df43a8e8d32a // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.32.0 // indirect golang.org/x/sync v0.19.0 // indirect diff --git a/go.sum b/go.sum index 897bb8a24..261390fcb 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfU github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= +github.com/skrashevich/go-webp v0.0.0-20260309074808-df43a8e8d32a h1:XlEMOOLLCc6IRVisVzFa/ajiYLL/O10Y8vYEnc+l4Y8= +github.com/skrashevich/go-webp v0.0.0-20260309074808-df43a8e8d32a/go.mod h1:9QtuNP/H9q/qzqgaZeYalNIk7n5lfyqVs1WTaPtC/Ao= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/internal/webp/webp.go b/internal/webp/webp.go new file mode 100644 index 000000000..368950768 --- /dev/null +++ b/internal/webp/webp.go @@ -0,0 +1,160 @@ +package webp + +import ( + "net/http" + "strconv" + "sync" + "time" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/ffmpeg" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/magic" + "github.com/AlexxIT/go2rtc/pkg/mjpeg" + "github.com/AlexxIT/go2rtc/pkg/webp" + "github.com/rs/zerolog" +) + +func Init() { + api.HandleFunc("api/frame.webp", handlerKeyframe) + api.HandleFunc("api/stream.webp", handlerStream) + + log = app.GetLogger("webp") +} + +var log zerolog.Logger + +var cache map[string]cacheEntry +var cacheMu sync.Mutex + +type cacheEntry struct { + payload []byte + timestamp time.Time +} + +func handlerKeyframe(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + stream, _ := streams.GetOrPatch(query) + if stream == nil { + http.Error(w, api.StreamNotFound, http.StatusNotFound) + return + } + + quality := 75 + if s := query.Get("quality"); s != "" { + if q, err := strconv.Atoi(s); err == nil && q > 0 && q <= 100 { + quality = q + } + } + + var b []byte + + if s := query.Get("cache"); s != "" { + if timeout, err := time.ParseDuration(s); err == nil { + src := query.Get("src") + + cacheMu.Lock() + entry, found := cache[src] + cacheMu.Unlock() + + if found && time.Since(entry.timestamp) < timeout { + writeWebPResponse(w, entry.payload) + return + } + + defer func() { + if b == nil { + return + } + entry = cacheEntry{payload: b, timestamp: time.Now()} + cacheMu.Lock() + if cache == nil { + cache = map[string]cacheEntry{src: entry} + } else { + cache[src] = entry + } + cacheMu.Unlock() + }() + } + } + + cons := magic.NewKeyframe() + cons.WithRequest(r) + + if err := stream.AddConsumer(cons); err != nil { + log.Error().Err(err).Caller().Send() + return + } + + once := &core.OnceBuffer{} + _, _ = cons.WriteTo(once) + b = once.Buffer() + + stream.RemoveConsumer(cons) + + var err error + switch cons.CodecName() { + case core.CodecH264, core.CodecH265: + ts := time.Now() + var jpegBytes []byte + if jpegBytes, err = ffmpeg.JPEGWithQuery(b, query); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Debug().Msgf("[webp] transcoding time=%s", time.Since(ts)) + if b, err = webp.EncodeJPEG(jpegBytes, quality); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case core.CodecJPEG: + fixed := mjpeg.FixJPEG(b) + if b, err = webp.EncodeJPEG(fixed, quality); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + writeWebPResponse(w, b) +} + +func writeWebPResponse(w http.ResponseWriter, b []byte) { + h := w.Header() + h.Set("Content-Type", "image/webp") + h.Set("Content-Length", strconv.Itoa(len(b))) + h.Set("Cache-Control", "no-cache") + h.Set("Connection", "close") + h.Set("Pragma", "no-cache") + + if _, err := w.Write(b); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func handlerStream(w http.ResponseWriter, r *http.Request) { + src := r.URL.Query().Get("src") + stream := streams.Get(src) + if stream == nil { + http.Error(w, api.StreamNotFound, http.StatusNotFound) + return + } + + cons := webp.NewConsumer() + cons.WithRequest(r) + + if err := stream.AddConsumer(cons); err != nil { + log.Error().Err(err).Msg("[api.webp] add consumer") + return + } + + h := w.Header() + h.Set("Cache-Control", "no-cache") + h.Set("Connection", "close") + h.Set("Pragma", "no-cache") + + wr := webp.NewWriter(w) + _, _ = cons.WriteTo(wr) + + stream.RemoveConsumer(cons) +} diff --git a/internal/webp/webp_test.go b/internal/webp/webp_test.go new file mode 100644 index 000000000..83bc8f1c7 --- /dev/null +++ b/internal/webp/webp_test.go @@ -0,0 +1,16 @@ +package webp + +import ( + "testing" +) + +func TestInit(t *testing.T) { + // Verify Init() runs without panicking and registers API endpoints. + // api.HandleFunc registrations are idempotent so calling Init multiple times is safe. + defer func() { + if r := recover(); r != nil { + t.Fatalf("Init() panicked: %v", r) + } + }() + Init() +} diff --git a/main.go b/main.go index 00c059e3e..030998626 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/kasa" "github.com/AlexxIT/go2rtc/internal/mjpeg" "github.com/AlexxIT/go2rtc/internal/mp4" + "github.com/AlexxIT/go2rtc/internal/webp" "github.com/AlexxIT/go2rtc/internal/mpeg" "github.com/AlexxIT/go2rtc/internal/multitrans" "github.com/AlexxIT/go2rtc/internal/nest" @@ -73,6 +74,7 @@ func main() { {"mp4", mp4.Init}, // MP4 API {"hls", hls.Init}, // HLS API {"mjpeg", mjpeg.Init}, // MJPEG API + {"webp", webp.Init}, // WebP API // Other sources and servers {"hass", hass.Init}, // hass source, Hass API server {"homekit", homekit.Init}, // homekit source, HomeKit server diff --git a/pkg/image/producer.go b/pkg/image/producer.go index 2081c048f..c99db7e50 100644 --- a/pkg/image/producer.go +++ b/pkg/image/producer.go @@ -1,13 +1,16 @@ package image import ( + "bytes" "errors" + "image/jpeg" "io" "net/http" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/pion/rtp" + webp "github.com/skrashevich/go-webp" ) type Producer struct { @@ -49,6 +52,12 @@ func (c *Producer) Start() error { return err } + if isWebP(body) { + if converted, err2 := webpToJPEG(body); err2 == nil { + body = converted + } + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: body, @@ -74,6 +83,12 @@ func (c *Producer) Start() error { return err } + if isWebP(body) { + if converted, err2 := webpToJPEG(body); err2 == nil { + body = converted + } + } + c.Recv += len(body) pkt = &rtp.Packet{ @@ -90,3 +105,23 @@ func (c *Producer) Stop() error { c.closed = true return c.Connection.Stop() } + +// isWebP returns true if data starts with RIFF....WEBP magic bytes. +func isWebP(data []byte) bool { + return len(data) >= 12 && + data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' && + data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P' +} + +// webpToJPEG decodes WebP bytes and re-encodes as JPEG. +func webpToJPEG(data []byte) ([]byte, error) { + img, err := webp.Decode(bytes.NewReader(data)) + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err = jpeg.Encode(&buf, img, nil); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/pkg/webp/consumer.go b/pkg/webp/consumer.go new file mode 100644 index 000000000..d90b5d44c --- /dev/null +++ b/pkg/webp/consumer.go @@ -0,0 +1,61 @@ +package webp + +import ( + "io" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Consumer struct { + core.Connection + wr *core.WriteBuffer +} + +func NewConsumer() *Consumer { + medias := []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecJPEG}, + {Name: core.CodecRAW}, + }, + }, + } + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "webp", + Medias: medias, + Transport: wr, + }, + wr: wr, + } +} + +func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + sender := core.NewSender(media, track.Codec) + sender.Handler = func(packet *rtp.Packet) { + if n, err := c.wr.Write(packet.Payload); err == nil { + c.Send += n + } + } + + if track.Codec.IsRTP() { + sender.Handler = RTPDepay(sender.Handler) + } else if track.Codec.Name == core.CodecRAW { + sender.Handler = Encoder(track.Codec, sender.Handler) + } else if track.Codec.Name == core.CodecJPEG { + sender.Handler = JPEGToWebP(sender.Handler) + } + + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { + return c.wr.WriteTo(wr) +} diff --git a/pkg/webp/helpers.go b/pkg/webp/helpers.go new file mode 100644 index 000000000..85b6b3501 --- /dev/null +++ b/pkg/webp/helpers.go @@ -0,0 +1,84 @@ +package webp + +import ( + "bytes" + "image" + "image/jpeg" + + webplib "github.com/skrashevich/go-webp" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/mjpeg" + "github.com/AlexxIT/go2rtc/pkg/y4m" + "github.com/pion/rtp" +) + +// EncodeImage encodes any image.Image to WebP lossy bytes. +func EncodeImage(img image.Image, quality int) ([]byte, error) { + buf := bytes.NewBuffer(nil) + if err := webplib.Encode(buf, img, &webplib.Options{Lossy: true, Quality: float32(quality)}); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// EncodeLossless encodes image.Image to WebP lossless bytes. +func EncodeLossless(img image.Image) ([]byte, error) { + buf := bytes.NewBuffer(nil) + if err := webplib.Encode(buf, img, &webplib.Options{Lossy: false}); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// EncodeJPEG converts JPEG bytes to WebP lossy bytes. +func EncodeJPEG(jpegData []byte, quality int) ([]byte, error) { + img, err := jpeg.Decode(bytes.NewReader(jpegData)) + if err != nil { + return nil, err + } + return EncodeImage(img, quality) +} + +// Decode decodes WebP bytes to image.Image. +func Decode(data []byte) (image.Image, error) { + return webplib.Decode(bytes.NewReader(data)) +} + +// FixJPEGToWebP is like mjpeg.FixJPEG but outputs WebP. Handles AVI1 MJPEG frames. +func FixJPEGToWebP(jpegData []byte, quality int) ([]byte, error) { + fixed := mjpeg.FixJPEG(jpegData) + return EncodeJPEG(fixed, quality) +} + +// Encoder converts a RAW YUV frame to WebP. +func Encoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + newImage := y4m.NewImage(codec.FmtpLine) + + return func(packet *rtp.Packet) { + img := newImage(packet.Payload) + + b, err := EncodeImage(img, 75) + if err != nil { + return + } + + clone := *packet + clone.Payload = b + handler(&clone) + } +} + +// JPEGToWebP converts a JPEG frame packet to WebP. +func JPEGToWebP(handler core.HandlerFunc) core.HandlerFunc { + return func(packet *rtp.Packet) { + b, err := EncodeJPEG(packet.Payload, 75) + if err != nil { + return + } + + clone := *packet + clone.Payload = b + handler(&clone) + } +} diff --git a/pkg/webp/rtp.go b/pkg/webp/rtp.go new file mode 100644 index 000000000..d4a5f7847 --- /dev/null +++ b/pkg/webp/rtp.go @@ -0,0 +1,11 @@ +package webp + +import ( + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/mjpeg" +) + +// RTPDepay depayloads RTP/JPEG packets and converts the resulting JPEG frame to WebP. +func RTPDepay(handler core.HandlerFunc) core.HandlerFunc { + return mjpeg.RTPDepay(JPEGToWebP(handler)) +} diff --git a/pkg/webp/webp_test.go b/pkg/webp/webp_test.go new file mode 100644 index 000000000..de4f10053 --- /dev/null +++ b/pkg/webp/webp_test.go @@ -0,0 +1,163 @@ +package webp + +import ( + "bytes" + "image" + "image/color" + "image/jpeg" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" +) + +func newTestImage(w, h int) *image.NRGBA { + img := image.NewNRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + img.SetNRGBA(x, y, color.NRGBA{R: uint8(x % 256), G: uint8(y % 256), B: 128, A: 255}) + } + } + return img +} + +func isWebP(data []byte) bool { + return len(data) >= 12 && + bytes.Equal(data[0:4], []byte("RIFF")) && + bytes.Equal(data[8:12], []byte("WEBP")) +} + +func TestEncodeImage(t *testing.T) { + img := newTestImage(100, 100) + data, err := EncodeImage(img, 75) + if err != nil { + t.Fatalf("EncodeImage error: %v", err) + } + if !isWebP(data) { + t.Fatalf("output is not valid WebP: got prefix %q", data[:min(12, len(data))]) + } +} + +func TestEncodeJPEG(t *testing.T) { + img := newTestImage(100, 100) + var jpegBuf bytes.Buffer + if err := jpeg.Encode(&jpegBuf, img, &jpeg.Options{Quality: 90}); err != nil { + t.Fatalf("jpeg.Encode error: %v", err) + } + data, err := EncodeJPEG(jpegBuf.Bytes(), 75) + if err != nil { + t.Fatalf("EncodeJPEG error: %v", err) + } + if !isWebP(data) { + t.Fatalf("output is not valid WebP: got prefix %q", data[:min(12, len(data))]) + } +} + +func TestDecode(t *testing.T) { + img := newTestImage(100, 80) + data, err := EncodeImage(img, 80) + if err != nil { + t.Fatalf("EncodeImage error: %v", err) + } + decoded, err := Decode(data) + if err != nil { + t.Fatalf("Decode error: %v", err) + } + bounds := decoded.Bounds() + if bounds.Dx() != 100 || bounds.Dy() != 80 { + t.Fatalf("expected 100x80, got %dx%d", bounds.Dx(), bounds.Dy()) + } +} + +func TestRoundTrip(t *testing.T) { + img := newTestImage(64, 64) + data, err := EncodeLossless(img) + if err != nil { + t.Fatalf("EncodeLossless error: %v", err) + } + decoded, err := Decode(data) + if err != nil { + t.Fatalf("Decode error: %v", err) + } + bounds := decoded.Bounds() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + orig := img.At(x, y) + got := decoded.At(x, y) + or, og, ob, oa := orig.RGBA() + gr, gg, gb, ga := got.RGBA() + if or != gr || og != gg || ob != gb || oa != ga { + t.Fatalf("pixel mismatch at (%d,%d): want %v got %v", x, y, orig, got) + } + } + } +} + +func TestEncodeLossless(t *testing.T) { + img := newTestImage(50, 50) + data, err := EncodeLossless(img) + if err != nil { + t.Fatalf("EncodeLossless error: %v", err) + } + if !isWebP(data) { + t.Fatalf("output is not valid WebP") + } + decoded, err := Decode(data) + if err != nil { + t.Fatalf("Decode error: %v", err) + } + bounds := decoded.Bounds() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + orig := img.At(x, y) + got := decoded.At(x, y) + or, og, ob, oa := orig.RGBA() + gr, gg, gb, ga := got.RGBA() + if or != gr || og != gg || ob != gb || oa != ga { + t.Fatalf("pixel mismatch at (%d,%d): want %v got %v", x, y, orig, got) + } + } + } +} + +func TestNewConsumer(t *testing.T) { + c := NewConsumer() + if c == nil { + t.Fatal("NewConsumer returned nil") + } + if c.FormatName != "webp" { + t.Fatalf("expected FormatName=webp, got %q", c.FormatName) + } + if len(c.Medias) == 0 { + t.Fatal("expected at least one media") + } + media := c.Medias[0] + if media.Kind != core.KindVideo { + t.Fatalf("expected KindVideo, got %v", media.Kind) + } + if media.Direction != core.DirectionSendonly { + t.Fatalf("expected DirectionSendonly, got %v", media.Direction) + } + hasJPEG := false + hasRAW := false + for _, codec := range media.Codecs { + if codec.Name == core.CodecJPEG { + hasJPEG = true + } + if codec.Name == core.CodecRAW { + hasRAW = true + } + } + if !hasJPEG { + t.Fatal("expected JPEG codec in consumer medias") + } + if !hasRAW { + t.Fatal("expected RAW codec in consumer medias") + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/pkg/webp/writer.go b/pkg/webp/writer.go new file mode 100644 index 000000000..a2705f620 --- /dev/null +++ b/pkg/webp/writer.go @@ -0,0 +1,38 @@ +package webp + +import ( + "io" + "net/http" + "strconv" +) + +const header = "--frame\r\nContent-Type: image/webp\r\nContent-Length: " + +// Writer writes multipart WebP frames to an HTTP response. +type Writer struct { + wr io.Writer + buf []byte +} + +// NewWriter creates a Writer that sets the multipart Content-Type header. +func NewWriter(w io.Writer) *Writer { + h := w.(http.ResponseWriter).Header() + h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame") + return &Writer{wr: w, buf: []byte(header)} +} + +func (w *Writer) Write(p []byte) (n int, err error) { + w.buf = w.buf[:len(header)] + w.buf = append(w.buf, strconv.Itoa(len(p))...) + w.buf = append(w.buf, "\r\n\r\n"...) + w.buf = append(w.buf, p...) + w.buf = append(w.buf, "\r\n"...) + + if _, err = w.wr.Write(w.buf); err != nil { + return 0, err + } + + w.wr.(http.Flusher).Flush() + + return len(p), nil +} diff --git a/website/api/openapi.yaml b/website/api/openapi.yaml index b61105724..777f329f1 100644 --- a/website/api/openapi.yaml +++ b/website/api/openapi.yaml @@ -567,6 +567,18 @@ paths: description: "" content: { multipart/x-mixed-replace: { example: "" } } + /api/stream.webp?src={src}: + get: + summary: Get stream in Motion-WebP format (multipart) + description: "Multipart stream of WebP frames. Pure Go encoding via [go-webp](https://github.com/skrashevich/go-webp)." + tags: [ Consume stream ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + responses: + 200: + description: "" + content: { multipart/x-mixed-replace: { example: "" } } + /api/stream.ascii?src={src}: get: summary: Get stream in ASCII-art format (ANSI escape codes) @@ -691,6 +703,55 @@ paths: content: image/jpeg: { example: "" } + /api/frame.webp?src={src}: + get: + summary: Get snapshot in WebP format + description: "Pure Go WebP encoding via [go-webp](https://github.com/skrashevich/go-webp). No FFmpeg or CGO required for the WebP conversion itself." + tags: [ Snapshot ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + - name: name + in: query + description: Optional stream name to create/update if `src` is a URL + required: false + schema: { type: string } + - name: quality + in: query + description: "WebP quality (1-100, default: 75)" + required: false + schema: { type: integer, minimum: 1, maximum: 100, default: 75 } + - name: width + in: query + description: "Scale output width (alias: `w`). Requires FFmpeg for H264/H265 sources." + required: false + schema: { type: integer, minimum: 1 } + - name: height + in: query + description: "Scale output height (alias: `h`). Requires FFmpeg for H264/H265 sources." + required: false + schema: { type: integer, minimum: 1 } + - name: rotate + in: query + description: "Rotate output (degrees). Requires FFmpeg for H264/H265 sources." + required: false + schema: { type: integer, enum: [ 90, 180, 270 ] } + - name: hardware + in: query + description: "Hardware acceleration engine for FFmpeg snapshot transcoding (alias: `hw`)" + required: false + schema: { type: string } + - name: cache + in: query + description: "Cache duration (e.g. `5s`, `1m`). Serves cached frame if within timeout." + required: false + schema: { type: string } + example: "5s" + responses: + "200": + description: "" + content: + image/webp: { example: "" } + /api/frame.mp4?src={src}: get: summary: Get snapshot in MP4 format diff --git a/www/links.html b/www/links.html index 13e08edfd..8be6cfe73 100644 --- a/www/links.html +++ b/www/links.html @@ -66,7 +66,9 @@

H264/H265 source

MJPEG source

  • stream.html with MJPEG mode / browsers: all / codecs: MJPEG, JPEG
  • stream.mjpeg MJPEG stream / browsers: all / codecs: MJPEG, JPEG
  • +
  • stream.webp Motion-WebP stream / browsers: all modern / codecs: MJPEG, JPEG
  • frame.jpeg snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG
  • +
  • frame.webp snapshot in WebP-format / browsers: all modern / codecs: H264, H265, MJPEG, JPEG
  • `; }); diff --git a/www/schema.json b/www/schema.json index 27fee57d7..0f84a47b3 100644 --- a/www/schema.json +++ b/www/schema.json @@ -151,6 +151,7 @@ "mp4", "hls", "mjpeg", + "webp", "hass", "homekit", "onvif", @@ -427,6 +428,9 @@ "mjpeg": { "$ref": "#/definitions/log_level" }, + "webp": { + "$ref": "#/definitions/log_level" + }, "mp4": { "$ref": "#/definitions/log_level" }, From 0d8d4c204d544f7a0783db4d960cdcd8759b1bf0 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 10 Mar 2026 22:54:14 +0300 Subject: [PATCH 3/4] fix(go.mod, go.sum): update go-webp dependency to v0.1.0 --- go.mod | 1 + go.sum | 14 ++++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 0f2354f23..12b686540 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f + github.com/skrashevich/go-webp v0.1.0 github.com/stretchr/testify v1.11.1 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 golang.org/x/crypto v0.47.0 diff --git a/go.sum b/go.sum index 261390fcb..fec54db26 100644 --- a/go.sum +++ b/go.sum @@ -106,26 +106,20 @@ github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfU github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= -github.com/skrashevich/go-webp v0.0.0-20260309074808-df43a8e8d32a h1:XlEMOOLLCc6IRVisVzFa/ajiYLL/O10Y8vYEnc+l4Y8= -github.com/skrashevich/go-webp v0.0.0-20260309074808-df43a8e8d32a/go.mod h1:9QtuNP/H9q/qzqgaZeYalNIk7n5lfyqVs1WTaPtC/Ao= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/skrashevich/go-webp v0.1.0 h1:C+dtldBorS5ISATYR5mvG9HFj8GRLDGRywT0xs/ZLUQ= +github.com/skrashevich/go-webp v0.1.0/go.mod h1:9QtuNP/H9q/qzqgaZeYalNIk7n5lfyqVs1WTaPtC/Ao= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= +golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= From f435b49652cbde710c4f707619e8555e1935d7bd Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 10 Mar 2026 22:56:42 +0300 Subject: [PATCH 4/4] chore(go.mod): remove unused dependencies - delete github.com/asticode/go-astits and github.com/asticode/go-astikit from go.mod and go.sum to clean up unused imports - update the project dependencies for better maintainability --- go.mod | 4 ---- go.sum | 41 +---------------------------------------- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/go.mod b/go.mod index 12b686540..bd0738133 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/AlexxIT/go2rtc go 1.26.1 require ( - github.com/asticode/go-astits v1.14.0 github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/expr-lang/expr v1.17.7 github.com/google/uuid v1.6.0 @@ -31,7 +30,6 @@ require ( ) require ( - github.com/asticode/go-astikit v0.57.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -40,11 +38,9 @@ require ( github.com/pion/mdns/v2 v2.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/sctp v1.9.2 // indirect - github.com/pion/transport/v3 v3.1.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/turn/v4 v4.1.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/skrashevich/go-webp v0.0.0-20260309074808-df43a8e8d32a // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.32.0 // indirect golang.org/x/sync v0.19.0 // indirect diff --git a/go.sum b/go.sum index fec54db26..dec9c0221 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,9 @@ -github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= -github.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA= -github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE= -github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00= -github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= -github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= -github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -32,24 +24,14 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= -github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= -github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= -github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM= -github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= -github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU= -github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4= github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw= github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4= -github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ= -github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU= github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ= github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -60,41 +42,26 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= -github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= -github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w= github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= -github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs= -github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY= github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo= github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8= -github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= -github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo= github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= -github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY= -github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8= github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ= github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M= -github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU= -github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA= github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA= -github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A= github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= -github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk= -github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s= github.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4= github.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -127,22 +94,16 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=