最近在折腾quic-go, 突然想起屏广适合用udp实现,而http3基于quic-go,后者又基于udp, 所以玩一下。
先贴出本机运行效果图:
功能(实现)说明:
1.服务器先启动作为共享屏幕方,等待客户端连接上来
2.客户端连接
3.客户端和服务器建立连接后,服务器主动打开stream
在一个for 循环中:每秒操作30次下面操作:
4.服务器开始抓取本机屏幕内容,转换成Image
5.数据传输协议:Image字节长度 + Image内容
6.客户端按上述协议接收数据,解析成Image对象,放界面上展示
服务端代码:
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"},
}
}
客户端代码:
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 时,运行服务器程序失败:
../../../../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环境…
或者有大佬知道这个问题能直接赐教吗?