diff --git a/cmd/olcrtc/main.go b/cmd/olcrtc/main.go index c24127c..eddb5ac 100644 --- a/cmd/olcrtc/main.go +++ b/cmd/olcrtc/main.go @@ -36,7 +36,8 @@ type config struct { videoFPS int videoBitrate string videoHW string - videoQRSize int + videoQRSize int + videoQRRecovery string videoCodec string videoBModule int videoBColors int @@ -118,6 +119,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.videoQRRecovery, "video-qr-recovery", "low", "QR error correction: low (7%), medium (15%), high (25%), highest (30%)") flag.StringVar(&cfg.videoCodec, "video-codec", "qrcode", "Visual codec: qrcode (slow, stable) or b (fast, unstable)") flag.IntVar(&cfg.videoBModule, "video-b-module", 4, "B codec pixels per module (default 4)") flag.IntVar(&cfg.videoBColors, "video-b-colors", 8, "B codec colors (4, 8, 16, 32, 64, 128, 256)") @@ -175,7 +177,8 @@ func toSessionConfig(cfg config) session.Config { VideoFPS: cfg.videoFPS, VideoBitrate: cfg.videoBitrate, VideoHW: cfg.videoHW, - VideoQRSize: cfg.videoQRSize, + VideoQRSize: cfg.videoQRSize, + VideoQRRecovery: cfg.videoQRRecovery, VideoCodec: cfg.videoCodec, VideoBModule: cfg.videoBModule, VideoBColors: cfg.videoBColors, diff --git a/go.mod b/go.mod index e019416..42cf885 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/openlibrecommunity/olcrtc go 1.25.0 require ( - github.com/boombuler/barcode v1.1.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/livekit/server-sdk-go/v2 v2.16.2 @@ -11,6 +10,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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/zarazaex69/b v0.0.0-20260423064626-c0bd20863b89 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 22a34eb..ca25dea 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= -github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -182,6 +180,8 @@ github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/internal/app/session/session.go b/internal/app/session/session.go index 6fe31e8..42b1a0f 100644 --- a/internal/app/session/session.go +++ b/internal/app/session/session.go @@ -77,7 +77,8 @@ type Config struct { VideoFPS int VideoBitrate string VideoHW string - VideoQRSize int + VideoQRSize int + VideoQRRecovery string VideoCodec string VideoBModule int VideoBColors int @@ -228,6 +229,7 @@ func Run(ctx context.Context, cfg Config) error { cfg.VideoBitrate, cfg.VideoHW, cfg.VideoQRSize, + cfg.VideoQRRecovery, cfg.VideoCodec, cfg.VP8FPS, cfg.VP8BatchSize, @@ -250,6 +252,7 @@ func Run(ctx context.Context, cfg Config) error { cfg.VideoBitrate, cfg.VideoHW, cfg.VideoQRSize, + cfg.VideoQRRecovery, cfg.VideoCodec, cfg.VP8FPS, cfg.VP8BatchSize, diff --git a/internal/client/client.go b/internal/client/client.go index eea7a0d..e8dcf0a 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -61,11 +61,12 @@ func Run( videoBitrate string, videoHW string, videoQRSize int, + videoQRRecovery string, 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, videoCodec, vp8FPS, vp8BatchSize) + return RunWithReady(ctx, linkName, transportName, carrierName, roomURL, keyHex, localAddr, dnsServer, socksUser, socksPass, nil, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, videoQRRecovery, videoCodec, vp8FPS, vp8BatchSize) } // RunWithReady is like Run but accepts a callback that is called when the client is ready. @@ -87,6 +88,7 @@ func RunWithReady( videoBitrate string, videoHW string, videoQRSize int, + videoQRRecovery string, videoCodec string, vp8FPS int, vp8BatchSize int, @@ -117,7 +119,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, videoCodec, vp8FPS, vp8BatchSize); err != nil { + if err := c.addLink(runCtx, linkName, transportName, carrierName, roomURL, i, cancel, dnsServer, "", 0, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, videoQRRecovery, videoCodec, vp8FPS, vp8BatchSize); err != nil { return fmt.Errorf("addLink failed: %w", err) } } @@ -221,28 +223,30 @@ func (c *Client) addLink( videoWidth, videoHeight, videoFPS int, videoBitrate, videoHW string, videoQRSize int, + videoQRRecovery string, videoCodec string, vp8FPS int, vp8BatchSize int, ) error { ln, err := link.New(ctx, linkName, link.Config{ - Transport: transportName, - Carrier: carrierName, - RoomURL: roomURL, - Name: names.Generate(), - OnData: c.onData, - DNSServer: dnsServer, - ProxyAddr: socksProxyAddr, - ProxyPort: socksProxyPort, - VideoWidth: videoWidth, - VideoHeight: videoHeight, - VideoFPS: videoFPS, - VideoBitrate: videoBitrate, - VideoHW: videoHW, - VideoQRSize: videoQRSize, - VideoCodec: videoCodec, - VP8FPS: vp8FPS, - VP8BatchSize: vp8BatchSize, + Transport: transportName, + Carrier: carrierName, + RoomURL: roomURL, + Name: names.Generate(), + OnData: c.onData, + DNSServer: dnsServer, + ProxyAddr: socksProxyAddr, + ProxyPort: socksProxyPort, + VideoWidth: videoWidth, + VideoHeight: videoHeight, + VideoFPS: videoFPS, + VideoBitrate: videoBitrate, + VideoHW: videoHW, + VideoQRSize: videoQRSize, + VideoQRRecovery: videoQRRecovery, + VideoCodec: videoCodec, + VP8FPS: vp8FPS, + VP8BatchSize: vp8BatchSize, }) if err != nil { return fmt.Errorf("failed to create link: %w", err) diff --git a/internal/link/direct/direct.go b/internal/link/direct/direct.go index 06840cd..ec66365 100644 --- a/internal/link/direct/direct.go +++ b/internal/link/direct/direct.go @@ -28,8 +28,9 @@ func New(ctx context.Context, cfg link.Config) (link.Link, error) { VideoFPS: cfg.VideoFPS, VideoBitrate: cfg.VideoBitrate, VideoHW: cfg.VideoHW, - VideoQRSize: cfg.VideoQRSize, - VideoCodec: cfg.VideoCodec, + VideoQRSize: cfg.VideoQRSize, + VideoQRRecovery: cfg.VideoQRRecovery, + VideoCodec: cfg.VideoCodec, VideoBModule: cfg.VideoBModule, VideoBColors: cfg.VideoBColors, VP8FPS: cfg.VP8FPS, diff --git a/internal/link/link.go b/internal/link/link.go index 59f538e..0c16146 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -38,8 +38,9 @@ type Config struct { VideoFPS int VideoBitrate string VideoHW string - VideoQRSize int - VideoCodec string + VideoQRSize int + VideoQRRecovery string + VideoCodec string VideoBModule int VideoBColors int VP8FPS int diff --git a/internal/server/server.go b/internal/server/server.go index eb00b7c..92c0c41 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -80,6 +80,7 @@ func Run( videoBitrate string, videoHW string, videoQRSize int, + videoQRRecovery string, videoCodec string, vp8FPS int, vp8BatchSize int, @@ -107,7 +108,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, videoCodec, vp8FPS, vp8BatchSize); err != nil { + if err := s.addLink(runCtx, linkName, transportName, carrierName, roomURL, i, cancel, videoWidth, videoHeight, videoFPS, videoBitrate, videoHW, videoQRSize, videoQRRecovery, videoCodec, vp8FPS, vp8BatchSize); err != nil { return fmt.Errorf("addLink failed: %w", err) } } @@ -194,28 +195,30 @@ func (s *Server) addLink( videoWidth, videoHeight, videoFPS int, videoBitrate, videoHW string, videoQRSize int, + videoQRRecovery string, videoCodec string, vp8FPS int, vp8BatchSize int, ) error { ln, err := link.New(ctx, linkName, link.Config{ - Transport: transportName, - Carrier: carrierName, - RoomURL: roomURL, - Name: names.Generate(), - OnData: s.onData, - DNSServer: s.dnsServer, - ProxyAddr: s.socksProxyAddr, - ProxyPort: s.socksProxyPort, - VideoWidth: videoWidth, - VideoHeight: videoHeight, - VideoFPS: videoFPS, - VideoBitrate: videoBitrate, - VideoHW: videoHW, - VideoQRSize: videoQRSize, - VideoCodec: videoCodec, - VP8FPS: vp8FPS, - VP8BatchSize: vp8BatchSize, + Transport: transportName, + Carrier: carrierName, + RoomURL: roomURL, + Name: names.Generate(), + OnData: s.onData, + DNSServer: s.dnsServer, + ProxyAddr: s.socksProxyAddr, + ProxyPort: s.socksProxyPort, + VideoWidth: videoWidth, + VideoHeight: videoHeight, + VideoFPS: videoFPS, + VideoBitrate: videoBitrate, + VideoHW: videoHW, + VideoQRSize: videoQRSize, + VideoQRRecovery: videoQRRecovery, + VideoCodec: videoCodec, + VP8FPS: vp8FPS, + VP8BatchSize: vp8BatchSize, }) if err != nil { return fmt.Errorf("failed to create link: %w", err) diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 67a1599..8c3aac0 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -46,8 +46,9 @@ type Config struct { VideoFPS int VideoBitrate string VideoHW string - VideoQRSize int - VideoCodec string + VideoQRSize int + VideoQRRecovery string + VideoCodec string VideoBModule int VideoBColors int VP8FPS int diff --git a/internal/transport/videochannel/transport.go b/internal/transport/videochannel/transport.go index 5769950..cbe273f 100644 --- a/internal/transport/videochannel/transport.go +++ b/internal/transport/videochannel/transport.go @@ -62,8 +62,9 @@ type streamTransport struct { videoFPS int videoBitrate string videoHW string - videoQRSize int - videoCodec string + videoQRSize int + videoQRRecovery string + videoCodec string videoBModule int videoBColors int } @@ -120,8 +121,9 @@ func New(ctx context.Context, cfg transport.Config) (transport.Transport, error) videoFPS: cfg.VideoFPS, videoBitrate: cfg.VideoBitrate, videoHW: cfg.VideoHW, - videoQRSize: qrSize, - videoCodec: cfg.VideoCodec, + videoQRSize: qrSize, + videoQRRecovery: cfg.VideoQRRecovery, + videoCodec: cfg.VideoCodec, videoBModule: cfg.VideoBModule, videoBColors: cfg.VideoBColors, } @@ -284,9 +286,9 @@ func (p *streamTransport) writerLoop() { var rawFrame []byte var err error if p.videoCodec == "b" { - rawFrame, err = renderVisualFrameB(payload, p.videoW, p.videoH, p.videoBModule, p.videoBColors) + rawFrame, err = renderVisualFrameB(payload, p.videoW, p.videoH, p.videoBModule, p.videoBColors, p.videoQRRecovery) } else { - rawFrame, err = renderVisualFrame(payload, p.videoW, p.videoH) + rawFrame, err = renderVisualFrame(payload, p.videoW, p.videoH, p.videoQRRecovery) } if err != nil { logger.Debugf("videochannel render error: %v", err) diff --git a/internal/transport/videochannel/visual.go b/internal/transport/videochannel/visual.go index 15e52aa..65b0c7c 100644 --- a/internal/transport/videochannel/visual.go +++ b/internal/transport/videochannel/visual.go @@ -6,20 +6,33 @@ import ( "image" "strings" - barcodedm "github.com/boombuler/barcode/datamatrix" "github.com/makiuchi-d/gozxing" - zxingdm "github.com/makiuchi-d/gozxing/datamatrix" + zxingqr "github.com/makiuchi-d/gozxing/qrcode" + qrgen "github.com/skip2/go-qrcode" ) const ( quietZone = 10 ) -func renderVisualFrame(payload []byte, width, height int) ([]byte, error) { +func parseRecoveryLevel(level string) qrgen.RecoveryLevel { + switch level { + case "medium": + return qrgen.Medium + case "high": + return qrgen.High + case "highest": + return qrgen.Highest + default: + return qrgen.Low + } +} + +func renderVisualFrame(payload []byte, width, height int, recoveryLevel string) ([]byte, error) { logicalFrameBytes := width * height frame := make([]byte, logicalFrameBytes) for i := range frame { - frame[i] = 0xff // White background + frame[i] = 0xff } if len(payload) == 0 { @@ -27,18 +40,16 @@ func renderVisualFrame(payload []byte, width, height int) ([]byte, error) { } encoded := base64.StdEncoding.EncodeToString(payload) - dm, err := barcodedm.Encode(encoded) + qr, err := qrgen.New(encoded, parseRecoveryLevel(recoveryLevel)) if err != nil { - return nil, fmt.Errorf("datamatrix encode: %w", err) + return nil, fmt.Errorf("qrcode encode: %w", err) } - // Use strict integer scaling to keep edges sharp - bounds := dm.Bounds() - dmW := bounds.Dx() - dmH := bounds.Dy() + bitmap := qr.Bitmap() + qrSize := len(bitmap) - scaleW := (width - (quietZone * 2)) / dmW - scaleH := (height - (quietZone * 2)) / dmH + scaleW := (width - (quietZone * 2)) / qrSize + scaleH := (height - (quietZone * 2)) / qrSize scale := scaleW if scaleH < scale { scale = scaleH @@ -47,16 +58,13 @@ func renderVisualFrame(payload []byte, width, height int) ([]byte, error) { scale = 1 } - totalW := dmW * scale - totalH := dmH * scale - offsetX := (width - totalW) / 2 - offsetY := (height - totalH) / 2 + totalSize := qrSize * scale + offsetX := (width - totalSize) / 2 + offsetY := (height - totalSize) / 2 - for y := 0; y < dmH; y++ { - for x := 0; x < dmW; x++ { - r, _, _, _ := dm.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA() - if r < 0x8000 { - // Fill scale x scale block + for y := 0; y < qrSize; y++ { + for x := 0; x < qrSize; x++ { + if bitmap[y][x] { for sy := 0; sy < scale; sy++ { for sx := 0; sx < scale; sx++ { pixelX := offsetX + (x * scale) + sx @@ -83,14 +91,13 @@ func extractVisualPayload(frame []byte, width, height int) ([]byte, error) { copy(img.Pix, frame) source := gozxing.NewLuminanceSourceFromImage(img) - // HybridBinarizer is good for noisy images binarizer := gozxing.NewHybridBinarizer(source) bmp, err := gozxing.NewBinaryBitmap(binarizer) if err != nil { return nil, fmt.Errorf("bitmap: %w", err) } - reader := zxingdm.NewDataMatrixReader() + reader := zxingqr.NewQRCodeReader() hints := make(map[gozxing.DecodeHintType]interface{}) hints[gozxing.DecodeHintType_TRY_HARDER] = true hints[gozxing.DecodeHintType_PURE_BARCODE] = true diff --git a/internal/transport/videochannel/visual_b.go b/internal/transport/videochannel/visual_b.go index e97c62b..688142f 100644 --- a/internal/transport/videochannel/visual_b.go +++ b/internal/transport/videochannel/visual_b.go @@ -10,7 +10,7 @@ import ( "github.com/zarazaex69/b/go" ) -func renderVisualFrameB(payload []byte, width, height, modulePx, colors int) ([]byte, error) { +func renderVisualFrameB(payload []byte, width, height, modulePx, colors int, _ string) ([]byte, error) { rgba := make([]byte, width*height*4) for i := 0; i < len(rgba); i += 4 { rgba[i] = 0xff diff --git a/internal/transport/videochannel/visual_b_stub.go b/internal/transport/videochannel/visual_b_stub.go index 036b9a9..c289eea 100644 --- a/internal/transport/videochannel/visual_b_stub.go +++ b/internal/transport/videochannel/visual_b_stub.go @@ -2,10 +2,10 @@ package videochannel -func renderVisualFrameB(payload []byte, width, height int) ([]byte, error) { - return renderVisualFrame(payload, width, height) +func renderVisualFrameB(payload []byte, width, height, modulePx, colors int, recoveryLevel string) ([]byte, error) { + return renderVisualFrame(payload, width, height, recoveryLevel) } -func extractVisualPayloadB(frame []byte, width, height int) ([]byte, error) { +func extractVisualPayloadB(frame []byte, width, height, modulePx, colors int) ([]byte, error) { return extractVisualPayload(frame, width, height) } diff --git a/mobile/mobile.go b/mobile/mobile.go index f9ee754..0aef9a5 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()