feat: add support for 'b' visual codec in videochannel

This commit is contained in:
zarazaex69
2026-04-22 21:26:50 +03:00
parent f124da9bbd
commit a2d1b95ffa
13 changed files with 146 additions and 6 deletions

View File

@@ -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,
}

1
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

View File

@@ -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,
)

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -39,6 +39,7 @@ type Config struct {
VideoBitrate string
VideoHW string
VideoQRSize int
VideoCodec string
VP8FPS int
VP8BatchSize int
}

View File

@@ -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,
})

View File

@@ -47,6 +47,7 @@ type Config struct {
VideoBitrate string
VideoHW string
VideoQRSize int
VideoCodec string
VP8FPS int
VP8BatchSize int
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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()