quic-go实现屏幕广播程序
最近在折腾quic-go, 突然想起屏广适合用udp实现,而http3基于quic-go,后者又基于udp, 所以玩一下。
先贴出本机运行效果图:
功能(实现)说明:
1.服务器先启动作为共享屏幕方,等待客户端连接上来
2.客户端连接
3.客户端和服务器建立连接后,服务器主动打开stream
在一个for 循环中:每秒操作30次下面操作:
4.服务器开始抓取本机屏幕内容,转换成Image5.数据传输协议:Image字节长度 + Image内容
6.客户端按上述协议接收数据,解析成Image对象,放界面上展示
服务端代码:
package mainimport ("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 MBMaxStreamReceiveWindow: 6 << 20, // 6 MBInitialConnectionReceiveWindow: 2 << 20, // 2 MBMaxConnectionReceiveWindow: 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 []bytefor {// 捕获桌面屏幕img, err := captureScreen()if err != nil {log.Fatal(err)}// 将图像编码为 PNG 格式var buf bytes.Buffererr = 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"},}
}
客户端代码:
package mainimport ("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 streamstream, 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.Picturepic := 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 时,运行服务器程序失败:
../../../../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+ ?
#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试试:
jelex@jelexxudeMacBook-Pro screenbroadcast % go get github.com/kbinani/screenshot@v0.0.0-20230831090513-3e604f0f372a
最后果然没问题了!
坑二:client程序无法交叉编译打包
我没有在windows电脑上验证,如果有使用windows版本的golang使用者看到本篇后,是否可以帮忙打包验证?
坑三:打包服务端程序成exe,在另一台电脑上运行,本机mac 作为客户端连接后没反应,直到超时报错退出:
2024/10/09 15:29:43 timeout: no recent network activity
是否有道友愿意联调?FYI: 我周边没有golang开发者,他们电脑上没安装golang环境…
或者有大佬知道这个问题能直接赐教吗?