mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-05-26 07:08:11 +00:00
Add configurable QR error correction level for video transpor
This commit is contained in:
@@ -37,6 +37,7 @@ type config struct {
|
||||
videoBitrate string
|
||||
videoHW string
|
||||
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)")
|
||||
@@ -176,6 +178,7 @@ func toSessionConfig(cfg config) session.Config {
|
||||
VideoBitrate: cfg.videoBitrate,
|
||||
VideoHW: cfg.videoHW,
|
||||
VideoQRSize: cfg.videoQRSize,
|
||||
VideoQRRecovery: cfg.videoQRRecovery,
|
||||
VideoCodec: cfg.videoCodec,
|
||||
VideoBModule: cfg.videoBModule,
|
||||
VideoBColors: cfg.videoBColors,
|
||||
|
||||
2
go.mod
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -78,6 +78,7 @@ type Config struct {
|
||||
VideoBitrate string
|
||||
VideoHW string
|
||||
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,
|
||||
|
||||
@@ -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,6 +223,7 @@ func (c *Client) addLink(
|
||||
videoWidth, videoHeight, videoFPS int,
|
||||
videoBitrate, videoHW string,
|
||||
videoQRSize int,
|
||||
videoQRRecovery string,
|
||||
videoCodec string,
|
||||
vp8FPS int,
|
||||
vp8BatchSize int,
|
||||
@@ -240,6 +243,7 @@ func (c *Client) addLink(
|
||||
VideoBitrate: videoBitrate,
|
||||
VideoHW: videoHW,
|
||||
VideoQRSize: videoQRSize,
|
||||
VideoQRRecovery: videoQRRecovery,
|
||||
VideoCodec: videoCodec,
|
||||
VP8FPS: vp8FPS,
|
||||
VP8BatchSize: vp8BatchSize,
|
||||
|
||||
@@ -29,6 +29,7 @@ func New(ctx context.Context, cfg link.Config) (link.Link, error) {
|
||||
VideoBitrate: cfg.VideoBitrate,
|
||||
VideoHW: cfg.VideoHW,
|
||||
VideoQRSize: cfg.VideoQRSize,
|
||||
VideoQRRecovery: cfg.VideoQRRecovery,
|
||||
VideoCodec: cfg.VideoCodec,
|
||||
VideoBModule: cfg.VideoBModule,
|
||||
VideoBColors: cfg.VideoBColors,
|
||||
|
||||
@@ -39,6 +39,7 @@ type Config struct {
|
||||
VideoBitrate string
|
||||
VideoHW string
|
||||
VideoQRSize int
|
||||
VideoQRRecovery string
|
||||
VideoCodec string
|
||||
VideoBModule int
|
||||
VideoBColors int
|
||||
|
||||
@@ -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,6 +195,7 @@ func (s *Server) addLink(
|
||||
videoWidth, videoHeight, videoFPS int,
|
||||
videoBitrate, videoHW string,
|
||||
videoQRSize int,
|
||||
videoQRRecovery string,
|
||||
videoCodec string,
|
||||
vp8FPS int,
|
||||
vp8BatchSize int,
|
||||
@@ -213,6 +215,7 @@ func (s *Server) addLink(
|
||||
VideoBitrate: videoBitrate,
|
||||
VideoHW: videoHW,
|
||||
VideoQRSize: videoQRSize,
|
||||
VideoQRRecovery: videoQRRecovery,
|
||||
VideoCodec: videoCodec,
|
||||
VP8FPS: vp8FPS,
|
||||
VP8BatchSize: vp8BatchSize,
|
||||
|
||||
@@ -47,6 +47,7 @@ type Config struct {
|
||||
VideoBitrate string
|
||||
VideoHW string
|
||||
VideoQRSize int
|
||||
VideoQRRecovery string
|
||||
VideoCodec string
|
||||
VideoBModule int
|
||||
VideoBColors int
|
||||
|
||||
@@ -63,6 +63,7 @@ type streamTransport struct {
|
||||
videoBitrate string
|
||||
videoHW string
|
||||
videoQRSize int
|
||||
videoQRRecovery string
|
||||
videoCodec string
|
||||
videoBModule int
|
||||
videoBColors int
|
||||
@@ -121,6 +122,7 @@ func New(ctx context.Context, cfg transport.Config) (transport.Transport, error)
|
||||
videoBitrate: cfg.VideoBitrate,
|
||||
videoHW: cfg.VideoHW,
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user