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环境...

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

相关推荐
小_太_阳43 分钟前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师1 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
搬码后生仔2 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱2 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3523 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生3 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟3 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
小蜗牛慢慢爬行4 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10435 小时前
java web springboot
java·spring boot·后端
龙少95437 小时前
【深入理解@EnableCaching】
java·后端·spring