mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-05-30 17:09:43 +00:00
feat: add support for 'b' visual codec in videochannel
This commit is contained in:
@@ -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
1
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
|
||||
)
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -39,6 +39,7 @@ type Config struct {
|
||||
VideoBitrate string
|
||||
VideoHW string
|
||||
VideoQRSize int
|
||||
VideoCodec string
|
||||
VP8FPS int
|
||||
VP8BatchSize int
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@ type Config struct {
|
||||
VideoBitrate string
|
||||
VideoHW string
|
||||
VideoQRSize int
|
||||
VideoCodec string
|
||||
VP8FPS int
|
||||
VP8BatchSize int
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
92
internal/transport/videochannel/visual_b.go
Normal file
92
internal/transport/videochannel/visual_b.go
Normal 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
|
||||
}
|
||||
11
internal/transport/videochannel/visual_b_stub.go
Normal file
11
internal/transport/videochannel/visual_b_stub.go
Normal 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)
|
||||
}
|
||||
@@ -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