From a2d1b95ffa6cfa40f134dbc6b97116fcf57b61d6 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Wed, 22 Apr 2026 21:26:50 +0300 Subject: [PATCH] feat: add support for 'b' visual codec in videochannel --- cmd/olcrtc/main.go | 3 + go.mod | 1 + go.sum | 2 + internal/app/session/session.go | 7 ++ internal/client/client.go | 8 +- internal/link/direct/direct.go | 1 + internal/link/link.go | 1 + internal/server/server.go | 5 +- internal/transport/transport.go | 1 + internal/transport/videochannel/transport.go | 18 +++- internal/transport/videochannel/visual_b.go | 92 +++++++++++++++++++ .../transport/videochannel/visual_b_stub.go | 11 +++ mobile/mobile.go | 2 +- 13 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 internal/transport/videochannel/visual_b.go create mode 100644 internal/transport/videochannel/visual_b_stub.go diff --git a/cmd/olcrtc/main.go b/cmd/olcrtc/main.go index 8bbeaba..ce82d96 100644 --- a/cmd/olcrtc/main.go +++ b/cmd/olcrtc/main.go @@ -37,6 +37,7 @@ type config struct { videoBitrate string videoHW string videoQRSize int + videoCodec string vp8FPS int vp8BatchSize int } @@ -115,6 +116,7 @@ func parseFlags() config { flag.StringVar(&cfg.videoBitrate, "video-bitrate", "", "Video bitrate (videochannel only)") flag.StringVar(&cfg.videoHW, "video-hw", "", "Hardware acceleration (none, nvenc)") flag.IntVar(&cfg.videoQRSize, "video-qr-size", 0, "Video QR code fragment size (videochannel only)") + flag.StringVar(&cfg.videoCodec, "video-codec", "qrcode", "Visual codec: qrcode (slow, stable) or b (fast, unstable)") flag.IntVar(&cfg.vp8FPS, "vp8-fps", 0, "VP8 frames per second (vp8channel only, default 25)") flag.IntVar(&cfg.vp8BatchSize, "vp8-batch", 0, "VP8 frames per tick (vp8channel only, default 1)") flag.Parse() @@ -170,6 +172,7 @@ func toSessionConfig(cfg config) session.Config { VideoBitrate: cfg.videoBitrate, VideoHW: cfg.videoHW, VideoQRSize: cfg.videoQRSize, + VideoCodec: cfg.videoCodec, VP8FPS: cfg.vp8FPS, VP8BatchSize: cfg.vp8BatchSize, } diff --git a/go.mod b/go.mod index cbd798f..cbd30ed 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/makiuchi-d/gozxing v0.1.1 github.com/pion/rtp v1.10.1 github.com/pion/webrtc/v4 v4.2.11 + github.com/zarazaex69/b v0.0.0-20260422171520-7eb386d13bda golang.org/x/crypto v0.50.0 golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b ) diff --git a/go.sum b/go.sum index 33b2008..933b33f 100644 --- a/go.sum +++ b/go.sum @@ -197,6 +197,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zarazaex69/b v0.0.0-20260422171520-7eb386d13bda h1:WVp2h2eFtWu/VU6HTq1Xh0VI/y5jI5svlxXIkIbXGEM= +github.com/zarazaex69/b v0.0.0-20260422171520-7eb386d13bda/go.mod h1:OUqzZNoXsg+ccaiAnSe0t4f8qc0W/cFx6io0lWsE1Gw= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= diff --git a/internal/app/session/session.go b/internal/app/session/session.go index b800531..91c348e 100644 --- a/internal/app/session/session.go +++ b/internal/app/session/session.go @@ -48,6 +48,7 @@ var ( ErrVideoFPSRequired = errors.New("video fps required for videochannel (use -video-fps)") ErrVideoBitrateRequired = errors.New("video bitrate required for videochannel (use -video-bitrate)") ErrVideoHWRequired = errors.New("video hardware acceleration required for videochannel (use -video-hw none/nvenc)") + ErrVideoCodecInvalid = errors.New("invalid video codec for videochannel (use -video-codec qrcode or -video-codec b)") // VP8channel errors ErrVP8FPSRequired = errors.New("vp8 fps required for vp8channel (use -vp8-fps)") @@ -77,6 +78,7 @@ type Config struct { VideoBitrate string VideoHW string VideoQRSize int + VideoCodec string VP8FPS int VP8BatchSize int } @@ -176,6 +178,9 @@ func Validate(cfg Config) error { if cfg.VideoHW == "" { return ErrVideoHWRequired } + if cfg.VideoCodec != "" && cfg.VideoCodec != "qrcode" && cfg.VideoCodec != "b" { + return ErrVideoCodecInvalid + } } if cfg.Transport == "vp8channel" { @@ -221,6 +226,7 @@ func Run(ctx context.Context, cfg Config) error { cfg.VideoBitrate, cfg.VideoHW, cfg.VideoQRSize, + cfg.VideoCodec, cfg.VP8FPS, cfg.VP8BatchSize, ) @@ -242,6 +248,7 @@ func Run(ctx context.Context, cfg Config) error { cfg.VideoBitrate, cfg.VideoHW, cfg.VideoQRSize, + cfg.VideoCodec, cfg.VP8FPS, cfg.VP8BatchSize, ) diff --git a/internal/client/client.go b/internal/client/client.go index d79153f..eea7a0d 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -61,10 +61,11 @@ func Run( videoBitrate string, videoHW string, videoQRSize int, + videoCodec string, vp8FPS int, vp8BatchSize int, ) error { - return RunWithReady(ctx, linkName, transportName, carrierName, roomURL, keyHex, localAddr, dnsServer, socksUser, socksPass, nil, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, vp8FPS, vp8BatchSize) + return RunWithReady(ctx, linkName, transportName, carrierName, roomURL, keyHex, localAddr, dnsServer, socksUser, socksPass, nil, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, videoCodec, vp8FPS, vp8BatchSize) } // RunWithReady is like Run but accepts a callback that is called when the client is ready. @@ -86,6 +87,7 @@ func RunWithReady( videoBitrate string, videoHW string, videoQRSize int, + videoCodec string, vp8FPS int, vp8BatchSize int, ) error { @@ -115,7 +117,7 @@ func RunWithReady( const linkCount = 1 for i := range linkCount { - if err := c.addLink(runCtx, linkName, transportName, carrierName, roomURL, i, cancel, dnsServer, "", 0, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, vp8FPS, vp8BatchSize); err != nil { + if err := c.addLink(runCtx, linkName, transportName, carrierName, roomURL, i, cancel, dnsServer, "", 0, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, videoCodec, vp8FPS, vp8BatchSize); err != nil { return fmt.Errorf("addLink failed: %w", err) } } @@ -219,6 +221,7 @@ func (c *Client) addLink( videoWidth, videoHeight, videoFPS int, videoBitrate, videoHW string, videoQRSize int, + videoCodec string, vp8FPS int, vp8BatchSize int, ) error { @@ -237,6 +240,7 @@ func (c *Client) addLink( VideoBitrate: videoBitrate, VideoHW: videoHW, VideoQRSize: videoQRSize, + VideoCodec: videoCodec, VP8FPS: vp8FPS, VP8BatchSize: vp8BatchSize, }) diff --git a/internal/link/direct/direct.go b/internal/link/direct/direct.go index da38955..703bd1b 100644 --- a/internal/link/direct/direct.go +++ b/internal/link/direct/direct.go @@ -29,6 +29,7 @@ func New(ctx context.Context, cfg link.Config) (link.Link, error) { VideoBitrate: cfg.VideoBitrate, VideoHW: cfg.VideoHW, VideoQRSize: cfg.VideoQRSize, + VideoCodec: cfg.VideoCodec, VP8FPS: cfg.VP8FPS, VP8BatchSize: cfg.VP8BatchSize, }) diff --git a/internal/link/link.go b/internal/link/link.go index 1a947fa..186f6e6 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -39,6 +39,7 @@ type Config struct { VideoBitrate string VideoHW string VideoQRSize int + VideoCodec string VP8FPS int VP8BatchSize int } diff --git a/internal/server/server.go b/internal/server/server.go index dd91a5e..eb00b7c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -80,6 +80,7 @@ func Run( videoBitrate string, videoHW string, videoQRSize int, + videoCodec string, vp8FPS int, vp8BatchSize int, ) error { @@ -106,7 +107,7 @@ func Run( const linkCount = 1 for i := range linkCount { - if err := s.addLink(runCtx, linkName, transportName, carrierName, roomURL, i, cancel, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, vp8FPS, vp8BatchSize); err != nil { + if err := s.addLink(runCtx, linkName, transportName, carrierName, roomURL, i, cancel, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, videoCodec, vp8FPS, vp8BatchSize); err != nil { return fmt.Errorf("addLink failed: %w", err) } } @@ -193,6 +194,7 @@ func (s *Server) addLink( videoWidth, videoHeight, videoFPS int, videoBitrate, videoHW string, videoQRSize int, + videoCodec string, vp8FPS int, vp8BatchSize int, ) error { @@ -211,6 +213,7 @@ func (s *Server) addLink( VideoBitrate: videoBitrate, VideoHW: videoHW, VideoQRSize: videoQRSize, + VideoCodec: videoCodec, VP8FPS: vp8FPS, VP8BatchSize: vp8BatchSize, }) diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 77bdb7f..c62e803 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -47,6 +47,7 @@ type Config struct { VideoBitrate string VideoHW string VideoQRSize int + VideoCodec string VP8FPS int VP8BatchSize int } diff --git a/internal/transport/videochannel/transport.go b/internal/transport/videochannel/transport.go index 6393fda..32aa233 100644 --- a/internal/transport/videochannel/transport.go +++ b/internal/transport/videochannel/transport.go @@ -63,6 +63,7 @@ type streamTransport struct { videoBitrate string videoHW string videoQRSize int + videoCodec string } // New creates a visual videochannel transport backed by a carrier-specific provider. @@ -118,6 +119,7 @@ func New(ctx context.Context, cfg transport.Config) (transport.Transport, error) videoBitrate: cfg.VideoBitrate, videoHW: cfg.VideoHW, videoQRSize: qrSize, + videoCodec: cfg.VideoCodec, } if err := stream.AddTrack(track); err != nil { @@ -275,7 +277,13 @@ func (p *streamTransport) writerLoop() { return } - rawFrame, err := renderVisualFrame(payload, p.videoW, p.videoH) + var rawFrame []byte + var err error + if p.videoCodec == "b" { + rawFrame, err = renderVisualFrameB(payload, p.videoW, p.videoH) + } else { + rawFrame, err = renderVisualFrame(payload, p.videoW, p.videoH) + } if err != nil { logger.Debugf("videochannel render error: %v", err) continue @@ -382,7 +390,13 @@ func (p *streamTransport) handleRemoteTrack(track *webrtc.TrackRemote, _ *webrtc } func (p *streamTransport) handleFrame(frame []byte) { - payload, err := extractVisualPayload(frame, p.videoW, p.videoH) + var payload []byte + var err error + if p.videoCodec == "b" { + payload, err = extractVisualPayloadB(frame, p.videoW, p.videoH) + } else { + payload, err = extractVisualPayload(frame, p.videoW, p.videoH) + } if err != nil || len(payload) == 0 { if err != nil { logger.Debugf("videochannel extract visual payload error: %v", err) diff --git a/internal/transport/videochannel/visual_b.go b/internal/transport/videochannel/visual_b.go new file mode 100644 index 0000000..b8b9701 --- /dev/null +++ b/internal/transport/videochannel/visual_b.go @@ -0,0 +1,92 @@ +//go:build b + +package videochannel + +import ( + "fmt" + + "github.com/zarazaex69/b/go" +) + +func renderVisualFrameB(payload []byte, width, height int) ([]byte, error) { + logicalFrameBytes := width * height + frame := make([]byte, logicalFrameBytes) + for i := range frame { + frame[i] = 0xff + } + + if len(payload) == 0 { + return frame, nil + } + + cfg := b.DefaultConfig() + result, err := b.Encode(payload, cfg) + if err != nil { + return nil, fmt.Errorf("b encode: %w", err) + } + + bmpW := int(result.Width) + bmpH := int(result.Height) + + scaleW := width / bmpW + scaleH := height / bmpH + scale := scaleW + if scaleH < scale { + scale = scaleH + } + if scale < 1 { + scale = 1 + } + + totalW := bmpW * scale + totalH := bmpH * scale + offsetX := (width - totalW) / 2 + offsetY := (height - totalH) / 2 + + for y := 0; y < bmpH; y++ { + for x := 0; x < bmpW; x++ { + idx := (y*bmpW + x) * 4 + r := result.RGBA[idx] + g := result.RGBA[idx+1] + bb := result.RGBA[idx+2] + + gray := uint8((int(r) + int(g) + int(bb)) / 3) + + for sy := 0; sy < scale; sy++ { + for sx := 0; sx < scale; sx++ { + pixelX := offsetX + (x * scale) + sx + pixelY := offsetY + (y * scale) + sy + if pixelX < width && pixelY < height { + frame[pixelY*width+pixelX] = gray + } + } + } + } + } + + return frame, nil +} + +func extractVisualPayloadB(frame []byte, width, height int) ([]byte, error) { + logicalFrameBytes := width * height + if len(frame) != logicalFrameBytes { + return nil, fmt.Errorf("unexpected frame size: %d (expected %dx%d=%d)", len(frame), width, height, logicalFrameBytes) + } + + rgba := make([]byte, width*height*4) + for i := 0; i < width*height; i++ { + gray := frame[i] + rgba[i*4] = gray + rgba[i*4+1] = gray + rgba[i*4+2] = gray + rgba[i*4+3] = 255 + } + + cfg := b.DefaultConfig() + decoded, err := b.Decode(rgba, uint32(width), uint32(height), cfg) + if err != nil { + return nil, nil + } + + return decoded, nil +} diff --git a/internal/transport/videochannel/visual_b_stub.go b/internal/transport/videochannel/visual_b_stub.go new file mode 100644 index 0000000..036b9a9 --- /dev/null +++ b/internal/transport/videochannel/visual_b_stub.go @@ -0,0 +1,11 @@ +//go:build !b + +package videochannel + +func renderVisualFrameB(payload []byte, width, height int) ([]byte, error) { + return renderVisualFrame(payload, width, height) +} + +func extractVisualPayloadB(frame []byte, width, height int) ([]byte, error) { + return extractVisualPayload(frame, width, height) +} diff --git a/mobile/mobile.go b/mobile/mobile.go index 3ab829b..f9ee754 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -123,7 +123,7 @@ func Start(roomID, keyHex string, socksPort int, socksUser, socksPass string) er close(localReady) }) }, - 0, 0, 0, "", "", 0, 0, 0, + 0, 0, 0, "", "", 0, "", 0, 0, ) mu.Lock()