quic-go实现屏幕广播程序

最近在折腾quic-go, 突然想起屏广适合用udp实现,而http3基于quic-go,后者又基于udp, 所以玩一下。

先贴出本机运行效果图:

功能(实现)说明:

1.服务器先启动作为共享屏幕方,等待客户端连接上来

2.客户端连接

3.客户端和服务器建立连接后,服务器主动打开stream

在一个for 循环中:每秒操作30次下面操作:

bash 复制代码
	4.服务器开始抓取本机屏幕内容,转换成Image
	5.数据传输协议:Image字节长度 + Image内容

6.客户端按上述协议接收数据,解析成Image对象,放界面上展示


服务端代码:

go 复制代码
package main

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/binary"
	"encoding/pem"
	"fmt"
	"github.com/quic-go/quic-go"
	"image"
	"image/png"
	"log"
	"math/big"
	"os"
	"time"

	"crypto/tls"
	"github.com/kbinani/screenshot"
)

const addr = "localhost:4000"

var currentDir, _ = os.Getwd()

var quicConf = &quic.Config{
	Allow0RTT:                      true,
	MaxIdleTimeout:                 40 * time.Second,
	InitialStreamReceiveWindow:     1 << 20,  // 1 MB
	MaxStreamReceiveWindow:         6 << 20,  // 6 MB
	InitialConnectionReceiveWindow: 2 << 20,  // 2 MB
	MaxConnectionReceiveWindow:     12 << 20, // 12 MB
}

func main() {
	//listener, err := quic.ListenAddr(addr, generateTLSConfig(), quicConf)
	listener, err := quic.ListenAddr(addr, generateTLSConfig2(), quicConf)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Server listening on", addr)

	for {
		// 接受客户端连接
		sess, err := listener.Accept(context.Background())
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("New client connected")
		go handleConnection(sess)
	}
}

func handleConnection(sess quic.Connection) {
	stream, err := sess.OpenStream()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("New stream opened:", stream.StreamID())
	defer stream.Close()
	var b []byte
	for {
		// 捕获桌面屏幕
		img, err := captureScreen()
		if err != nil {
			log.Fatal(err)
		}

		// 将图像编码为 PNG 格式
		var buf bytes.Buffer
		err = png.Encode(&buf, img)
		if err != nil {
			log.Fatal(err)
		}

		// magic校验
		//n, err := stream.Write([]byte{0x05, 0x19})
		//if err != nil {
		//	log.Fatal(err)
		//}
		b = buf.Bytes()
		//var headLenBuf = make([]byte, 4)
		//binary.BigEndian.PutUint32(headLenBuf, uint32(len(b)))
		//_, err = stream.Write(headLenBuf)
		err = binary.Write(stream, binary.BigEndian, uint32(len(b)))
		if err != nil {
			log.Fatal(err)
		}
		// 将图像数据发送到客户端
		_, err = stream.Write(b)
		if err != nil {
			log.Fatal(err)
		}
		// 每秒捕获并传输一帧
		time.Sleep(1 * time.Second / 30)
	}
}

func captureScreen() (image.Image, error) {
	bounds := screenshot.GetDisplayBounds(0) // 捕获主屏幕
	img, err := screenshot.CaptureRect(bounds)
	if err != nil {
		return nil, err
	}
	return img, nil
}

/*
*
openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes
*/
func generateTLSConfig() *tls.Config {
	// 使用自签名证书
	// goland运行使用它
	cert, err := tls.LoadX509KeyPair(currentDir+"/screenbroadcast/cert.pem", currentDir+"/screenbroadcast/privkey.pem")
	// 命令行运行使用它
	//cert, err := tls.LoadX509KeyPair("cert.pem", "privkey.pem")
	if err != nil {
		log.Fatal(err)
	}
	return &tls.Config{
		Certificates: []tls.Certificate{cert},
		NextProtos:   []string{"h3-29"},
	}
}

func generateTLSConfig2() *tls.Config {
	key, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		panic(err)
	}
	template := x509.Certificate{SerialNumber: big.NewInt(1)}
	certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
	if err != nil {
		panic(err)
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})

	tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		panic(err)
	}
	return &tls.Config{
		Certificates: []tls.Certificate{tlsCert},
		NextProtos:   []string{"h3-29"},
	}
}

客户端代码:

go 复制代码
package main

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/binary"
	"fmt"
	"github.com/quic-go/quic-go"
	"image"
	"image/png"
	"io"
	"log"
	"time"

	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
)

const addr = "localhost:4000"

var headLenBuf = make([]byte, 4)

func main() {
	pixelgl.Run(run)
}

func run() {
	tlsConf := &tls.Config{
		InsecureSkipVerify: true,
		NextProtos:         []string{"h3-29"},
	}

	quicConfig := &quic.Config{
		MaxIdleTimeout:  40 * time.Second,
		KeepAlivePeriod: 30 * time.Second, // 使用quic的心跳机制
	}
	// 创建 QUIC 连接到服务器
	sess, err := quic.DialAddr(context.Background(), addr, tlsConf, quicConfig)
	if err != nil {
		log.Fatal(err)
	}

	// 接收一个 QUIC stream:没错,是server主动推送数据过来,先发起的open stream
	stream, err := sess.AcceptStream(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// 创建窗口显示接收的屏幕图像
	cfg := pixelgl.WindowConfig{
		Title:     "Screen Broadcast",
		Bounds:    pixel.R(0, 0, 1024, 680),
		VSync:     true,
		Resizable: true,
	}
	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		log.Fatal(err)
	}
	for !win.Closed() {
		// 接收图像数据
		img, err := receiveImage(stream)
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Fatal(err)
		}

		// 将图像转换为 pixel.Picture
		pic := pixel.PictureDataFromImage(img)

		// 绘制图像
		sprite := pixel.NewSprite(pic, pic.Bounds())
		win.Clear(pixel.RGB(0, 0, 0))
		sprite.Draw(win, pixel.IM.Moved(win.Bounds().Center()))
		win.Update()
	}
}

func receiveImage(stream quic.Stream) (image.Image, error) {
	//_, err := io.ReadFull(stream, headLenBuf[:2])
	//if err != nil {
	//	return nil, err
	//}
	//if headLenBuf[0] != 0x05 && headLenBuf[1] != 0x19 {
	//	return nil, errors.New("invalid magic")
	//}
	_, err := io.ReadFull(stream, headLenBuf)
	if err != nil {
		fmt.Println("video Error reading:", err.Error())
		return nil, err
	}
	headLen := binary.BigEndian.Uint32(headLenBuf)
	var buf bytes.Buffer
	// 从 QUIC stream 读取图像数据
	_, err = io.CopyN(&buf, stream, int64(headLen))
	if err != nil {
		return nil, err
	}

	// 解码 PNG 图像
	img, err := png.Decode(&buf)
	if err != nil {
		return nil, err
	}

	return img, nil
}

下面开始说其中涉及到的坑:

当我本机(mac m1) OS版本为 12.1 时,运行服务器程序失败:

bash 复制代码
../../../../go/pkg/mod/github.com/kbinani/screenshot@v0.0.0-20240820160931-a8a2c5d0e191/darwin.go:9:10: fatal error:
'ScreenCaptureKit/ScreenCaptureKit.h' file not found
#include <ScreenCaptureKit/ScreenCaptureKit.h>

网上说升级系统到12.3+,因为ScreenCaptureKit 是 macOS 12.3 及更高版本中引入的 API,用于捕获屏幕内容。但是我升级到12.7.6后仍然报错...

然后看github.com/kbinani/screenshot源码:我当前下载的screenshot版本需要14.4+ ?

go 复制代码
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > MAC_OS_VERSION_14_4

FYI:我不敢升级到15版本,,,不敢。。。只是小版本升级

最后解决办法:使用低版本的screenshot:

去官网:https://pkg.go.dev/github.com/kbinani/screenshot@v0.0.0-20240820160931-a8a2c5d0e191/example?tab=versions

使用低版本的2023试试:

go 复制代码
jelex@jelexxudeMacBook-Pro screenbroadcast % go get github.com/kbinani/screenshot@v0.0.0-20230831090513-3e604f0f372a

最后果然没问题了!

坑二:client程序无法交叉编译打包

我没有在windows电脑上验证,如果有使用windows版本的golang使用者看到本篇后,是否可以帮忙打包验证?

坑三:打包服务端程序成exe,在另一台电脑上运行,本机mac 作为客户端连接后没反应,直到超时报错退出:
go 复制代码
2024/10/09 15:29:43 timeout: no recent network activity

是否有道友愿意联调?FYI: 我周边没有golang开发者,他们电脑上没安装golang环境...

或者有大佬知道这个问题能直接赐教吗?

相关推荐
雪隐41 分钟前
个人电脑玩AI00-前言
人工智能·后端
我是一颗柠檬1 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
前端Hardy1 小时前
CSS 动画真的比 JS 快?Josh Comeau 做了组实验,结果跟直觉不一样
前端·javascript·后端
Front思1 小时前
调取支付宝支付正式环境不可以唤起来,但是沙箱可以
后端
foggyprojects1 小时前
AI 生成 SQL 模板以后,为什么还需要固定 helper 规则
后端
明天一点1 小时前
Cloudflare 通知转发钉钉机器人
前端·后端
前端Hardy1 小时前
前端日历组件,要变天了?Schedule-X v4.6 彻底杀疯了
前端·javascript·后端
Oo_行者_oO1 小时前
微服务 Feign 从“万能公共服务”到“业务客户端”
后端·架构
wei_shuo1 小时前
别再踩坑了!KingbaseES 存储过程与触发器开发避坑实录
后端
元宝骑士1 小时前
MySQL 实战:跨表排序 + 指定类型置顶四种写法
后端·mysql