Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7dd665cea1 | ||
|
3812babc58 | ||
|
c64db4a7a5 | ||
|
f1fe154e01 | ||
|
3af8cf2090 | ||
|
935d5ade76 | ||
|
07d3ae3717 | ||
|
4844944c37 | ||
|
1be49c0dd2 | ||
|
708732a37b | ||
|
b945218c85 | ||
|
c22c399f40 | ||
|
dc9b5ce0b1 | ||
|
acf74153ef | ||
|
5c93e698b6 | ||
|
095b52fb5d | ||
|
64584cde86 | ||
|
a597aef7ea | ||
|
24ae8d7657 | ||
|
de24203100 | ||
|
b7b7e78614 | ||
|
edbe406f5b | ||
|
7ef721afb7 | ||
|
39a89b80e2 | ||
|
18f5d649d1 | ||
|
821c0cffb8 | ||
|
bd0812cc42 | ||
|
4c80f8a25e | ||
|
0ce8374276 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
config.yaml
|
||||
*.mp4
|
||||
test
|
||||
test.*
|
||||
experimental
|
||||
dist
|
||||
.DS_Store
|
@ -1,3 +1,3 @@
|
||||
# ace
|
||||
# ACE Cloud Emulator
|
||||
|
||||
ACE Cloud Emulator
|
||||
A webrtc based qemu UI
|
21
TODO
Normal file
21
TODO
Normal file
@ -0,0 +1,21 @@
|
||||
# 2022-9-28
|
||||
1. 视频分辨率切换问题->弃用vnc
|
||||
2. 还是没声音,以及panic
|
||||
3. 整体重构!
|
||||
4. 综合调试
|
||||
|
||||
# 2022-9-30
|
||||
1. 视频没有图像
|
||||
2. qemu只在声卡初始化后才开始抓取声音
|
||||
|
||||
# 2022-10-3
|
||||
1. 虚拟设备应当一直运行,无阻塞
|
||||
|
||||
# 2022-10-10
|
||||
1. 视频设备延迟过高
|
||||
|
||||
# 2022-10-11
|
||||
1. 解决视频设备延迟->丢帧机制?或者使用tmpfs->RGB到YUV时,双for循环慢,平均在30-50ms
|
||||
|
||||
# 2022-10-18
|
||||
1. 视频稳定在15ms左右,但大分辨率仍然慢
|
74
cmd/config/config.go
Normal file
74
cmd/config/config.go
Normal file
@ -0,0 +1,74 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.sense-t.eu.org/ACE/ace/servers/qemuserver"
|
||||
"git.sense-t.eu.org/ACE/ace/servers/webserver"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var Command *cli.Command
|
||||
|
||||
type Config struct {
|
||||
Debug bool `yaml:"debug"`
|
||||
WEBServer *webserver.Options `yaml:"webserver"`
|
||||
Qemu *qemuserver.Options `yaml:"qemu"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
Command = &cli.Command{
|
||||
Name: "config",
|
||||
Usage: "config file options",
|
||||
Aliases: []string{"conf", "cfg"},
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "generate",
|
||||
Usage: "generate config file",
|
||||
Action: genconf,
|
||||
Aliases: []string{"gen"},
|
||||
},
|
||||
{
|
||||
Name: "check",
|
||||
Usage: "check config file",
|
||||
Action: checkconf,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Debug: true,
|
||||
WEBServer: webserver.ExampleOptions(),
|
||||
Qemu: qemuserver.ExampleOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
func genconf(c *cli.Context) error {
|
||||
f, err := os.OpenFile(c.String("config"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return yaml.NewEncoder(f).Encode(NewConfig())
|
||||
}
|
||||
|
||||
func checkconf(c *cli.Context) error {
|
||||
_, err := ReadConfig(c)
|
||||
return err
|
||||
}
|
||||
|
||||
func ReadConfig(c *cli.Context) (*Config, error) {
|
||||
f, err := os.OpenFile(c.String("config"), os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
config := &Config{}
|
||||
err = yaml.NewDecoder(f).Decode(config)
|
||||
return config, err
|
||||
}
|
73
cmd/server/server.go
Normal file
73
cmd/server/server.go
Normal file
@ -0,0 +1,73 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.sense-t.eu.org/ACE/ace/cmd/config"
|
||||
"git.sense-t.eu.org/ACE/ace/servers/qemuserver"
|
||||
"git.sense-t.eu.org/ACE/ace/servers/webserver"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
options *config.Config
|
||||
}
|
||||
|
||||
var Command *cli.Command
|
||||
|
||||
func init() {
|
||||
Command = &cli.Command{
|
||||
Name: "server",
|
||||
Usage: "run ace server",
|
||||
Action: runServer,
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(c *config.Config) (*Server, error) {
|
||||
return &Server{
|
||||
options: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
if s.options.Debug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
qemu, err := qemuserver.NewServer(s.options.Qemu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webServer, err := webserver.NewServer(s.options.WEBServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webServer.RTCConnector.QEMU = qemu
|
||||
|
||||
go func() {
|
||||
if err := qemu.Run(); err != nil {
|
||||
logrus.Fatal("cannot run qemuserver with error: ", err)
|
||||
}
|
||||
}()
|
||||
logrus.Debug("qemu server running")
|
||||
|
||||
return webServer.Run()
|
||||
}
|
||||
|
||||
func runServer(c *cli.Context) error {
|
||||
config, err := config.ReadConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := NewServer(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return server.Run()
|
||||
}
|
93
drivers/audio/wavfifo.go
Normal file
93
drivers/audio/wavfifo.go
Normal file
@ -0,0 +1,93 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/mediadevices/pkg/wave"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
DataChunkIDSize = 4
|
||||
DataChunkSizeSize = 4
|
||||
)
|
||||
|
||||
// BufferSize for pcm bytes
|
||||
const BitsPerByte = 8
|
||||
|
||||
type PCMStreamDriver struct {
|
||||
PCM <-chan []byte
|
||||
BufferSizeByBytes uint16
|
||||
WaveHeader *WavHeader
|
||||
closed <-chan struct{}
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (w *PCMStreamDriver) Open() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
w.closed = ctx.Done()
|
||||
w.cancel = cancel
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *PCMStreamDriver) Close() error {
|
||||
w.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *PCMStreamDriver) Properties() []prop.Media {
|
||||
logrus.Debugf("wave header: %v", w.WaveHeader)
|
||||
return []prop.Media{
|
||||
{
|
||||
Audio: prop.Audio{
|
||||
SampleRate: int(w.WaveHeader.SampleRate),
|
||||
ChannelCount: int(w.WaveHeader.NumChannels),
|
||||
SampleSize: int(w.WaveHeader.BitsPerSample),
|
||||
Latency: w.WaveHeader.GetLatnecy(w.BufferSizeByBytes),
|
||||
IsFloat: false, // just 8bit or 16bit with qemu
|
||||
IsBigEndian: false, // qemu should be little endian
|
||||
IsInterleaved: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (w *PCMStreamDriver) AudioRecord(p prop.Media) (audio.Reader, error) {
|
||||
logrus.Debug(p)
|
||||
chunkInfo := wave.ChunkInfo{
|
||||
Len: int(w.BufferSizeByBytes) / int(p.SampleSize/BitsPerByte),
|
||||
Channels: p.ChannelCount,
|
||||
SamplingRate: p.SampleRate,
|
||||
}
|
||||
|
||||
reader := func() (wave.Audio, func(), error) {
|
||||
a := wave.NewInt16Interleaved(chunkInfo)
|
||||
ticker := time.NewTicker(p.Latency)
|
||||
defer ticker.Stop()
|
||||
select {
|
||||
case <-w.closed:
|
||||
return nil, func() {}, io.EOF
|
||||
case pcmData := <-w.PCM:
|
||||
copy(a.Data, bytesTo16BitSamples(pcmData[:]))
|
||||
case <-ticker.C:
|
||||
// no stuck
|
||||
}
|
||||
return a, func() {}, nil
|
||||
}
|
||||
return audio.ReaderFunc(reader), nil
|
||||
}
|
||||
|
||||
func bytesTo16BitSamples(b []byte) []int16 {
|
||||
samples := make([]int16, 0)
|
||||
for i := 0; i < len(b); i += 2 {
|
||||
sample := binary.LittleEndian.Uint16(b[i : i+2])
|
||||
samples = append(samples, int16(sample))
|
||||
}
|
||||
return samples
|
||||
}
|
124
drivers/audio/wavheader.go
Normal file
124
drivers/audio/wavheader.go
Normal file
@ -0,0 +1,124 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Skip riff header and `fmt ` just 16 bytes
|
||||
const (
|
||||
FmtHeaderOffset = 0x0c // 12
|
||||
FmtHeaderIDSize = 4
|
||||
FmtHeaderChunkSizeSize = 4
|
||||
FmtHeaderSizeDefault = 16
|
||||
)
|
||||
|
||||
type WavHeader struct {
|
||||
ID [4]byte
|
||||
Size uint32
|
||||
AudioFormat uint16
|
||||
NumChannels uint16
|
||||
SampleRate uint32
|
||||
ByteRate uint32
|
||||
BlockAlign uint16
|
||||
BitsPerSample uint16
|
||||
}
|
||||
|
||||
func NewHeader(f io.Reader) (*WavHeader, error) {
|
||||
w := &WavHeader{}
|
||||
if err := w.Parse(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func DefaultHeader() *WavHeader {
|
||||
return &WavHeader{
|
||||
Size: uint32(FmtHeaderSizeDefault),
|
||||
AudioFormat: 1,
|
||||
NumChannels: 2,
|
||||
SampleRate: 48000, // opus only support 48kHz
|
||||
BlockAlign: 4,
|
||||
BitsPerSample: 16,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WavHeader) Parse(f io.Reader) error {
|
||||
// skip headers
|
||||
var _headers [FmtHeaderOffset]byte
|
||||
if _, err := f.Read(_headers[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var id [4]byte
|
||||
if _, err := f.Read(id[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(id[:], []byte("fmt ")) {
|
||||
return errors.New("bad header")
|
||||
}
|
||||
w.ID = id
|
||||
|
||||
var size [4]byte
|
||||
if _, err := f.Read(size[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Size = binary.LittleEndian.Uint32(size[:])
|
||||
|
||||
var af [2]byte
|
||||
if _, err := f.Read(af[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.AudioFormat = binary.LittleEndian.Uint16(af[:])
|
||||
|
||||
var nc [2]byte
|
||||
if _, err := f.Read(nc[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.NumChannels = binary.LittleEndian.Uint16(nc[:])
|
||||
|
||||
var sr [4]byte
|
||||
if _, err := f.Read(sr[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.SampleRate = binary.LittleEndian.Uint32(sr[:])
|
||||
|
||||
var br [4]byte
|
||||
if _, err := f.Read(br[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.ByteRate = binary.LittleEndian.Uint32(br[:])
|
||||
|
||||
var ba [2]byte
|
||||
if _, err := f.Read(ba[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.BlockAlign = binary.LittleEndian.Uint16(ba[:])
|
||||
|
||||
var bps [2]byte
|
||||
if _, err := f.Read(bps[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.BitsPerSample = binary.LittleEndian.Uint16(bps[:])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WavHeader) String() string {
|
||||
b, _ := json.Marshal(w)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (w *WavHeader) GetLatnecy(bufferSizeByBytes uint16) time.Duration {
|
||||
bytesPerSample := w.BitsPerSample / BitsPerByte
|
||||
bufferLength := bufferSizeByBytes / bytesPerSample
|
||||
|
||||
return time.Second *
|
||||
time.Duration(bufferLength) /
|
||||
time.Duration(w.NumChannels) /
|
||||
time.Duration(w.SampleRate)
|
||||
}
|
102
drivers/video/ppm.go
Normal file
102
drivers/video/ppm.go
Normal file
@ -0,0 +1,102 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"image/draw"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
_ "github.com/jbuchbinder/gopnm"
|
||||
)
|
||||
|
||||
const DefaultFPS float32 = 60.0
|
||||
|
||||
type Frame struct {
|
||||
Time time.Time
|
||||
Image io.ReadCloser
|
||||
}
|
||||
|
||||
type PPMStreamDriver struct {
|
||||
Height, Width int
|
||||
FPS float32
|
||||
PPMImage <-chan Frame
|
||||
closed <-chan struct{}
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (v *PPMStreamDriver) Open() error {
|
||||
defer logrus.Debug("device opened")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
v.closed = ctx.Done()
|
||||
v.cancel = cancel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *PPMStreamDriver) Close() error {
|
||||
v.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *PPMStreamDriver) Properties() []prop.Media {
|
||||
return []prop.Media{
|
||||
{
|
||||
Video: prop.Video{
|
||||
Width: v.Width,
|
||||
Height: v.Height,
|
||||
FrameRate: v.FPS,
|
||||
FrameFormat: frame.FormatYUYV,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (v *PPMStreamDriver) VideoRecord(p prop.Media) (video.Reader, error) {
|
||||
logrus.Debug(p)
|
||||
|
||||
canvas := image.NewRGBA(image.Rect(0, 0, p.Width, p.Height))
|
||||
var (
|
||||
prevHeight, prevWidth int
|
||||
)
|
||||
|
||||
r := video.ReaderFunc(func() (img image.Image, release func(), err error) {
|
||||
select {
|
||||
case <-v.closed:
|
||||
return nil, func() {}, io.EOF
|
||||
case ppmF := <-v.PPMImage:
|
||||
defer ppmF.Image.Close()
|
||||
|
||||
// skip timeouted frame
|
||||
if time.Since(ppmF.Time) > time.Second/time.Duration(p.FrameRate) {
|
||||
return canvas, func() {}, nil
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(ppmF.Image)
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
// screen geometroy change
|
||||
if img.Bounds().Dx() != prevWidth || img.Bounds().Dy() != prevHeight {
|
||||
draw.Draw(canvas, canvas.Rect, image.Black, image.Black.Bounds().Min, draw.Over)
|
||||
prevWidth = img.Bounds().Dx()
|
||||
prevHeight = img.Bounds().Dy()
|
||||
}
|
||||
|
||||
offsetX := (canvas.Rect.Dx() - img.Bounds().Dx()) / 2
|
||||
offsetY := (canvas.Rect.Dy() - img.Bounds().Dy()) / 2
|
||||
|
||||
draw.Draw(canvas, image.Rect(
|
||||
offsetX, offsetY, offsetX+img.Bounds().Dx(), offsetY+img.Bounds().Dy(),
|
||||
), img, img.Bounds().Min, draw.Over)
|
||||
default:
|
||||
}
|
||||
return canvas, func() {}, nil
|
||||
})
|
||||
return r, nil
|
||||
}
|
61
go.mod
Normal file
61
go.mod
Normal file
@ -0,0 +1,61 @@
|
||||
module git.sense-t.eu.org/ACE/ace
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/jbuchbinder/gopnm v0.0.0-20220507095634-e31f54490ce0
|
||||
github.com/pion/mediadevices v0.3.11
|
||||
github.com/urfave/cli/v2 v2.11.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/pion/datachannel v1.5.2 // indirect
|
||||
github.com/pion/dtls/v2 v2.1.5 // indirect
|
||||
github.com/pion/ice/v2 v2.2.6 // indirect
|
||||
github.com/pion/interceptor v0.1.12 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/rtp v1.7.13 // indirect
|
||||
github.com/pion/sctp v1.8.2 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.5 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.10 // indirect
|
||||
github.com/pion/stun v0.3.5 // indirect
|
||||
github.com/pion/transport v0.13.1 // indirect
|
||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||
github.com/pion/udp v0.1.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/digitalocean/go-qemu v0.0.0-20220804221245-2002801203aa
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/pion/webrtc/v3 v3.1.43
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
256
go.sum
Normal file
256
go.sum
Normal file
@ -0,0 +1,256 @@
|
||||
github.com/blackjack/webcam v0.0.0-20220329180758-ba064708e165/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e h1:SCnqm8SjSa0QqRxXbo5YY//S+OryeJioe17nK+iDZpg=
|
||||
github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e/go.mod h1:o129ljs6alsIQTc8d6eweihqpmmrbxZ2g1jhgjhPykI=
|
||||
github.com/digitalocean/go-qemu v0.0.0-20220804221245-2002801203aa h1:MiYy1HQgJcyj9OZpsrJThjUTN15mId070zUZTfHqCnY=
|
||||
github.com/digitalocean/go-qemu v0.0.0-20220804221245-2002801203aa/go.mod h1:GUMRsCBc7W7BzUF0zizFJ7Pna929325F+v9hvnWD+pI=
|
||||
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gen2brain/malgo v0.10.35/go.mod h1:zHSUNZAXfCeNsZou0RtQ6Zk7gDYLIcKOrUWtAdksnEs=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jbuchbinder/gopnm v0.0.0-20220507095634-e31f54490ce0 h1:9GwwkVzUn1vRWAQ8GRu7UOaoM+FZGnvw88DsjyiqfXc=
|
||||
github.com/jbuchbinder/gopnm v0.0.0-20220507095634-e31f54490ce0/go.mod h1:6U0E76+sB1jTuSSXJjePtLd44vExeoYThOWgOoXo3x8=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329/go.mod h1:2VPVQDR4wO7KXHwP+DAypEy67rXf+okUx2zjgpCxZw4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
||||
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
github.com/pion/mediadevices v0.3.11 h1:1orX6LMr+byAMRiulzXEGsodHYtHNvcv13U2VAeJI0o=
|
||||
github.com/pion/mediadevices v0.3.11/go.mod h1:sKLty4bEcD45Q+cp4tbIGflx4veFrbMHuIg0+5HWWe4=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
|
||||
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA=
|
||||
github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
86
lib/qemuconnection/events.go
Normal file
86
lib/qemuconnection/events.go
Normal file
@ -0,0 +1,86 @@
|
||||
package qemuconnection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/digitalocean/go-qemu/qmp"
|
||||
)
|
||||
|
||||
const (
|
||||
MouseMoveEvent = iota
|
||||
MouseButtonEvent
|
||||
KeyboardEvent
|
||||
ControlEvent
|
||||
QueryStatusEvent
|
||||
)
|
||||
|
||||
const (
|
||||
ControlEventRestart = iota
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type int `json:"type"`
|
||||
Args map[string]any `json:"args"`
|
||||
}
|
||||
|
||||
type CommandLine struct {
|
||||
Command string `json:"command-line"`
|
||||
}
|
||||
|
||||
func ParseEvent(b []byte) (*Event, error) {
|
||||
event := &Event{}
|
||||
if err := json.Unmarshal(b, event); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (e *Event) ToQemuCommand() []qmp.Command {
|
||||
switch e.Type {
|
||||
case MouseMoveEvent:
|
||||
return []qmp.Command{
|
||||
makeHMCommand("mouse_move %d %d %d", e.Args["dx"], e.Args["dy"], e.Args["dz"]),
|
||||
}
|
||||
case MouseButtonEvent:
|
||||
return []qmp.Command{
|
||||
makeHMCommand("mouse_button %d", e.Args["button"]),
|
||||
}
|
||||
case KeyboardEvent:
|
||||
return []qmp.Command{
|
||||
makeHMCommand("sendkey %s", e.Args["key"]),
|
||||
}
|
||||
case ControlEvent:
|
||||
t, ok := e.Args["cmd"].(int)
|
||||
if ok {
|
||||
return makeControlCommand(t)
|
||||
}
|
||||
case QueryStatusEvent:
|
||||
return []qmp.Command{
|
||||
{
|
||||
Execute: "query-status",
|
||||
},
|
||||
}
|
||||
}
|
||||
return make([]qmp.Command, 0)
|
||||
}
|
||||
|
||||
func makeControlCommand(t int) []qmp.Command {
|
||||
switch t {
|
||||
case ControlEventRestart:
|
||||
return []qmp.Command{
|
||||
makeHMCommand("system_reset"),
|
||||
makeHMCommand("cont"),
|
||||
}
|
||||
}
|
||||
return make([]qmp.Command, 0)
|
||||
}
|
||||
|
||||
func makeHMCommand(cmdTemplate string, args ...any) qmp.Command {
|
||||
return qmp.Command{
|
||||
Execute: "human-monitor-command",
|
||||
Args: CommandLine{
|
||||
Command: fmt.Sprintf(cmdTemplate, args...),
|
||||
},
|
||||
}
|
||||
}
|
134
lib/webrtcconnection/connection.go
Normal file
134
lib/webrtcconnection/connection.go
Normal file
@ -0,0 +1,134 @@
|
||||
package webrtcconnection
|
||||
|
||||
import (
|
||||
"git.sense-t.eu.org/ACE/ace/servers/qemuserver"
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/codec/opus"
|
||||
"github.com/pion/mediadevices/pkg/codec/x264"
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const DefaultStreamID = "ace-server"
|
||||
|
||||
type Connection struct {
|
||||
option *Options
|
||||
api *webrtc.API
|
||||
stream mediadevices.MediaStream
|
||||
QEMU *qemuserver.Server
|
||||
}
|
||||
|
||||
func New(o *Options) (*Connection, error) {
|
||||
connection := &Connection{
|
||||
option: o,
|
||||
}
|
||||
codecSelector, err := setupCodec(o.VideoBPS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
me := &webrtc.MediaEngine{}
|
||||
codecSelector.Populate(me)
|
||||
|
||||
connection.api = webrtc.NewAPI(webrtc.WithMediaEngine(me))
|
||||
|
||||
logrus.Debug("list devices:")
|
||||
devices := driver.GetManager().Query(func(d driver.Driver) bool { return true })
|
||||
for _, device := range devices {
|
||||
logrus.Debug("\t", device.ID())
|
||||
logrus.Debug("\t", device.Info())
|
||||
logrus.Debug("\t", device.Properties())
|
||||
logrus.Debug("\t", device.Status())
|
||||
logrus.Debug("\t------")
|
||||
}
|
||||
|
||||
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(mtc *mediadevices.MediaTrackConstraints) {},
|
||||
Audio: func(mtc *mediadevices.MediaTrackConstraints) {},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connection.stream = s
|
||||
|
||||
return connection, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Regist(offer *webrtc.SessionDescription) (*webrtc.SessionDescription, error) {
|
||||
logrus.Debug("received offer ")
|
||||
|
||||
rtc, err := c.api.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: c.option.STUNServers,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||
logrus.Debug("connection state has changed: ", connectionState.String())
|
||||
})
|
||||
|
||||
rtc.OnICECandidate(func(i *webrtc.ICECandidate) {
|
||||
logrus.Debug("cadidate: ", i)
|
||||
})
|
||||
|
||||
for _, track := range c.stream.GetTracks() {
|
||||
track.OnEnded(func(err error) {
|
||||
logrus.Errorf("Track (ID: %s, kind: %s) ended with error: %v", track.ID(), track.Kind().String(), err)
|
||||
})
|
||||
_, err := rtc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
rtc.OnDataChannel(c.dataChannel)
|
||||
|
||||
if err := rtc.SetRemoteDescription(*offer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logrus.Debug("offer set")
|
||||
|
||||
answer, err := rtc.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := rtc.SetLocalDescription(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logrus.Debug("answer set")
|
||||
|
||||
<-webrtc.GatheringCompletePromise(rtc)
|
||||
|
||||
defer logrus.Debug("regist complete")
|
||||
return rtc.LocalDescription(), nil
|
||||
}
|
||||
|
||||
func setupCodec(videoBPS int) (*mediadevices.CodecSelector, error) {
|
||||
x264Prarm, err := x264.NewParams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x264Prarm.BitRate = videoBPS
|
||||
x264Prarm.Preset = x264.PresetFaster
|
||||
|
||||
opusParam, err := opus.NewParams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithAudioEncoders(&opusParam),
|
||||
mediadevices.WithVideoEncoders(&x264Prarm),
|
||||
)
|
||||
return codecSelector, nil
|
||||
}
|
49
lib/webrtcconnection/datachannel.go
Normal file
49
lib/webrtcconnection/datachannel.go
Normal file
@ -0,0 +1,49 @@
|
||||
package webrtcconnection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (c *Connection) dataChannel(d *webrtc.DataChannel) {
|
||||
d.OnOpen(func() {
|
||||
for {
|
||||
status := c.QEMU.GetStatus().String()
|
||||
currentTime := time.Now().UnixMilli()
|
||||
|
||||
b, err := json.Marshal(map[string]any{
|
||||
"qemu_status": status,
|
||||
"server_time": currentTime,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf(
|
||||
"failed to parse to json on '%s-%d' with error: %v",
|
||||
d.Label(), *d.ID(), err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := d.Send(b); err != nil {
|
||||
logrus.Errorf(
|
||||
"failed to send qemu status to '%s-%d' with error: %v",
|
||||
d.Label(), *d.ID(), err,
|
||||
)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
})
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
logrus.Debugf("received %d bytes message from '%s-%d'", len(msg.Data), d.Label(), *d.ID())
|
||||
if !msg.IsString {
|
||||
return
|
||||
}
|
||||
if err := c.QEMU.SendEvent(msg.Data); err != nil {
|
||||
logrus.Errorf(
|
||||
"cannot parse message from '%s-%d' to qemu controll event: %v",
|
||||
d.Label(), *d.ID(), err,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
17
lib/webrtcconnection/options.go
Normal file
17
lib/webrtcconnection/options.go
Normal file
@ -0,0 +1,17 @@
|
||||
package webrtcconnection
|
||||
|
||||
type Options struct {
|
||||
STUNServers []string `yaml:"stun_servers"`
|
||||
VideoBPS int `yaml:"video_bps"`
|
||||
}
|
||||
|
||||
func ExampleOptions() *Options {
|
||||
options := &Options{
|
||||
STUNServers: []string{
|
||||
"stun:stun.l.google.com:19302",
|
||||
"stun:wetofu.me:3478",
|
||||
},
|
||||
VideoBPS: 2_048_000,
|
||||
}
|
||||
return options
|
||||
}
|
38
main.go
Normal file
38
main.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.sense-t.eu.org/ACE/ace/cmd/config"
|
||||
"git.sense-t.eu.org/ACE/ace/cmd/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
logrus.SetReportCaller(true)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "ace",
|
||||
Usage: "cloud emulator",
|
||||
Commands: []*cli.Command{
|
||||
config.Command,
|
||||
server.Command,
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Usage: "config file",
|
||||
Aliases: []string{"c"},
|
||||
EnvVars: []string{"ACE_CONFIG"},
|
||||
Value: "config.yaml",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
logrus.Panic(err)
|
||||
}
|
||||
}
|
58
servers/qemuserver/options.go
Normal file
58
servers/qemuserver/options.go
Normal file
@ -0,0 +1,58 @@
|
||||
package qemuserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
QmpAddress string `yaml:"address"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
Name string `yaml:"name"`
|
||||
Audio AudioOptions `yaml:"audio_options"`
|
||||
Video VideoOptions `yaml:"video_options"`
|
||||
}
|
||||
|
||||
type AudioOptions struct {
|
||||
Device string `yaml:"device"`
|
||||
BufferSize uint16 `yaml:"buffer_size"` // in bytes
|
||||
}
|
||||
|
||||
type VideoOptions struct {
|
||||
Height int `yaml:"height"`
|
||||
Width int `yaml:"width"`
|
||||
FPS float32 `yaml:"fps"`
|
||||
}
|
||||
|
||||
func ExampleOptions() *Options {
|
||||
return &Options{
|
||||
QmpAddress: (&url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: "localhost:4444",
|
||||
}).String(),
|
||||
Timeout: time.Duration(30 * time.Second),
|
||||
Name: "ace-qemu",
|
||||
Audio: AudioOptions{
|
||||
Device: "snd0",
|
||||
BufferSize: 2048,
|
||||
},
|
||||
Video: VideoOptions{
|
||||
Height: 768,
|
||||
Width: 1024,
|
||||
FPS: 30,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Options) MakeFIFO() (string, error) {
|
||||
path := path.Join(
|
||||
os.TempDir(),
|
||||
fmt.Sprintf("%s-%s-audio", o.Name, o.Audio.Device),
|
||||
)
|
||||
os.Remove(path)
|
||||
return path, syscall.Mkfifo(path, 0600)
|
||||
}
|
218
servers/qemuserver/server.go
Normal file
218
servers/qemuserver/server.go
Normal file
@ -0,0 +1,218 @@
|
||||
package qemuserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.sense-t.eu.org/ACE/ace/drivers/audio"
|
||||
"git.sense-t.eu.org/ACE/ace/drivers/video"
|
||||
"git.sense-t.eu.org/ACE/ace/lib/qemuconnection"
|
||||
"github.com/digitalocean/go-qemu/qemu"
|
||||
"github.com/digitalocean/go-qemu/qmp"
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const waveHeaderSize = audio.FmtHeaderSizeDefault
|
||||
|
||||
var waveHeader *audio.WavHeader
|
||||
|
||||
func init() {
|
||||
waveHeader = audio.DefaultHeader()
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
options *Options
|
||||
qemu *qemu.Domain
|
||||
audioHeader chan *audio.WavHeader
|
||||
pcm chan []byte
|
||||
ppm chan video.Frame
|
||||
}
|
||||
|
||||
var DefaultServer *Server
|
||||
|
||||
func NewServer(o *Options) (*Server, error) {
|
||||
server := &Server{
|
||||
options: o,
|
||||
audioHeader: make(chan *audio.WavHeader, 1),
|
||||
pcm: make(chan []byte),
|
||||
ppm: make(chan video.Frame), // to be configured
|
||||
}
|
||||
|
||||
u, err := url.Parse(o.QmpAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var address string
|
||||
if u.Scheme == "unix" {
|
||||
address = u.Path
|
||||
} else {
|
||||
address = u.Host
|
||||
}
|
||||
logrus.Debugf("trying to connect qmp with %s://%s", u.Scheme, address)
|
||||
qmpConnection, err := qmp.NewSocketMonitor(u.Scheme, address, o.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := qmpConnection.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logrus.Debug("qmp connected")
|
||||
|
||||
qemu, err := qemu.NewDomain(qmpConnection, o.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server.qemu = qemu
|
||||
|
||||
audio := &audio.PCMStreamDriver{
|
||||
PCM: server.pcm,
|
||||
WaveHeader: waveHeader,
|
||||
BufferSizeByBytes: o.Audio.BufferSize, // to be configured
|
||||
}
|
||||
if err := driver.GetManager().Register(
|
||||
audio,
|
||||
driver.Info{
|
||||
Label: "audioFifo",
|
||||
DeviceType: driver.Microphone,
|
||||
Priority: driver.PriorityNormal,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
video := &video.PPMStreamDriver{
|
||||
Height: o.Video.Height,
|
||||
Width: o.Video.Width,
|
||||
FPS: o.Video.FPS,
|
||||
PPMImage: server.ppm,
|
||||
}
|
||||
if err := driver.GetManager().Register(
|
||||
video,
|
||||
driver.Info{
|
||||
Label: "vnc",
|
||||
DeviceType: driver.Camera,
|
||||
Priority: driver.PriorityNormal,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
logrus.Debug("qemu server running")
|
||||
path, err := s.options.MakeFIFO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
logrus.Debug("screen capture start")
|
||||
defer close(s.ppm)
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(s.options.Video.FPS))
|
||||
defer ticker.Stop()
|
||||
|
||||
for { // to be configured
|
||||
now := time.Now()
|
||||
ppm, err := s.qemu.ScreenDump()
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case s.ppm <- video.Frame{
|
||||
Time: now,
|
||||
Image: ppm,
|
||||
}:
|
||||
<-ticker.C
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
logrus.Debug("start reading fifo")
|
||||
|
||||
logrus.Debug("skip wave headers, to the PCM!")
|
||||
// skip to pcm data, for 44 bytes.
|
||||
var _dataChunkHeader [audio.FmtHeaderOffset +
|
||||
audio.FmtHeaderIDSize + audio.FmtHeaderChunkSizeSize + waveHeaderSize +
|
||||
audio.DataChunkIDSize + audio.DataChunkSizeSize]byte
|
||||
if _, err := f.Read(_dataChunkHeader[:]); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debug("start reading PCM")
|
||||
defer close(s.pcm)
|
||||
ticker := time.NewTicker(waveHeader.GetLatnecy(s.options.Audio.BufferSize))
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
|
||||
b := make([]byte, s.options.Audio.BufferSize) // to be configured
|
||||
if _, err := f.Read(b[:]); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
select {
|
||||
case s.pcm <- b:
|
||||
<-ticker.C
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
logrus.Debug("setting audio capture")
|
||||
if _, err := s.qemu.Run(qmp.Command{
|
||||
Execute: "human-monitor-command",
|
||||
Args: map[string]string{
|
||||
"command-line": fmt.Sprintf(
|
||||
"wavcapture %s %s %d %d %d",
|
||||
path,
|
||||
s.options.Audio.Device,
|
||||
waveHeader.SampleRate,
|
||||
waveHeader.BitsPerSample,
|
||||
waveHeader.NumChannels,
|
||||
),
|
||||
},
|
||||
}); err != nil {
|
||||
logrus.Fatal("run audio command failed: ", err)
|
||||
}
|
||||
logrus.Debug("audio capture set")
|
||||
}()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func (s *Server) SendEvent(b []byte) error {
|
||||
ev, err := qemuconnection.ParseEvent(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cmd := range ev.ToQemuCommand() {
|
||||
_, err := s.qemu.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) GetStatus() qemu.Status {
|
||||
status, err := s.qemu.Status()
|
||||
if err != nil {
|
||||
return qemu.StatusIOError
|
||||
}
|
||||
return status
|
||||
}
|
70
servers/webserver/handlers.go
Normal file
70
servers/webserver/handlers.go
Normal file
@ -0,0 +1,70 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Succeed bool `json:"succeed"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (s *Server) getInstruction(c *gin.Context) {
|
||||
f, _ := os.ReadFile(s.options.InstructionFile)
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Succeed: true,
|
||||
Message: "",
|
||||
Data: string(f),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) exchangeSDP(c *gin.Context) {
|
||||
offer := &webrtc.SessionDescription{}
|
||||
if err := c.BindJSON(offer); err != nil {
|
||||
c.JSON(http.StatusBadRequest, Response{
|
||||
Succeed: false,
|
||||
Message: "bad request",
|
||||
Data: err.Error(),
|
||||
})
|
||||
logrus.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
answer, err := s.RTCConnector.Regist(offer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, Response{
|
||||
Succeed: false,
|
||||
Message: "internal error",
|
||||
Data: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Succeed: true,
|
||||
Message: "",
|
||||
Data: answer,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getICEConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Succeed: true,
|
||||
Message: "",
|
||||
Data: s.options.WebRTC.STUNServers,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getName(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Succeed: true,
|
||||
Message: "",
|
||||
Data: s.options.Name,
|
||||
})
|
||||
}
|
21
servers/webserver/options.go
Normal file
21
servers/webserver/options.go
Normal file
@ -0,0 +1,21 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"git.sense-t.eu.org/ACE/ace/lib/webrtcconnection"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Name string `yaml:"name"`
|
||||
Listen string `yaml:"listen"`
|
||||
InstructionFile string `yaml:"instruction_file"`
|
||||
WebRTC *webrtcconnection.Options `yaml:"webrtc"`
|
||||
}
|
||||
|
||||
func ExampleOptions() *Options {
|
||||
return &Options{
|
||||
Name: "ACE-test",
|
||||
Listen: "localhost:8080",
|
||||
InstructionFile: "",
|
||||
WebRTC: webrtcconnection.ExampleOptions(),
|
||||
}
|
||||
}
|
30
servers/webserver/routers.go
Normal file
30
servers/webserver/routers.go
Normal file
@ -0,0 +1,30 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Server) setupRoute() {
|
||||
apiHandler := gin.New()
|
||||
groupV1 := apiHandler.Group("/api/v1")
|
||||
{
|
||||
groupV1.
|
||||
POST("/sdp", s.exchangeSDP).
|
||||
GET("/instruction", s.getInstruction).
|
||||
GET("/iceserver/url", s.getICEConfig).
|
||||
GET("/name", s.getName)
|
||||
}
|
||||
|
||||
s.webServer.Use(func(ctx *gin.Context) {
|
||||
path := ctx.Request.RequestURI
|
||||
logrus.Debug(path)
|
||||
if strings.HasPrefix(path, "/api") {
|
||||
apiHandler.HandleContext(ctx)
|
||||
} else {
|
||||
staticFileHandler()(ctx)
|
||||
}
|
||||
})
|
||||
}
|
34
servers/webserver/server.go
Normal file
34
servers/webserver/server.go
Normal file
@ -0,0 +1,34 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"git.sense-t.eu.org/ACE/ace/lib/webrtcconnection"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
options *Options
|
||||
webServer *gin.Engine
|
||||
RTCConnector *webrtcconnection.Connection
|
||||
}
|
||||
|
||||
func NewServer(o *Options) (*Server, error) {
|
||||
rtc, err := webrtcconnection.New(o.WebRTC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
options: o,
|
||||
webServer: gin.New(),
|
||||
RTCConnector: rtc,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
logrus.Debug("webserver running")
|
||||
defer logrus.Debug("webserver exit")
|
||||
s.setupRoute()
|
||||
return s.webServer.Run(s.options.Listen)
|
||||
}
|
35
servers/webserver/static.go
Normal file
35
servers/webserver/static.go
Normal file
@ -0,0 +1,35 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
//go:generate cp -r ../../web/dist ./
|
||||
//go:embed dist
|
||||
var staticFiles embed.FS
|
||||
|
||||
func staticFileHandler() gin.HandlerFunc {
|
||||
sf, err := fs.Sub(staticFiles, "dist")
|
||||
if err != nil {
|
||||
logrus.Fatal("compile error: ", err)
|
||||
}
|
||||
|
||||
fs := http.FileServer(http.FS(sf))
|
||||
|
||||
return func(ctx *gin.Context) {
|
||||
defer ctx.Abort()
|
||||
filename := strings.TrimLeft(ctx.Request.RequestURI, "/")
|
||||
|
||||
if filename == "" {
|
||||
filename = "index.html"
|
||||
}
|
||||
|
||||
fs.ServeHTTP(ctx.Writer, ctx.Request)
|
||||
}
|
||||
}
|
23
web/.gitignore
vendored
Normal file
23
web/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
24
web/README.md
Normal file
24
web/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# web
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
web/babel.config.js
Normal file
5
web/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
22
web/jsconfig.json
Normal file
22
web/jsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"experimentalDisableTemplateSupport": true
|
||||
}
|
||||
}
|
48
web/package.json
Normal file
48
web/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"core-js": "^3.8.3",
|
||||
"marked": "^4.1.0",
|
||||
"naive-ui": "^2.33.2",
|
||||
"vue": "^3.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser",
|
||||
"requireConfigFile": false
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
],
|
||||
"_id": "web@0.1.0",
|
||||
"readme": "ERROR: No README data found!"
|
||||
}
|
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
web/public/index.html
Normal file
17
web/public/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
27
web/src/App.vue
Normal file
27
web/src/App.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<n-message-provider placement="top-right">
|
||||
<main-app />
|
||||
</n-message-provider>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import MainApp from "./components/MainApp.vue";
|
||||
import { NMessageProvider } from "naive-ui";
|
||||
import { onMounted } from "vue";
|
||||
import { store } from "./store";
|
||||
|
||||
const setTitle = async () => {
|
||||
document.title = await store.loadTitle();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTitle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
16
web/src/ajax/ajax.js
Normal file
16
web/src/ajax/ajax.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const ajax = async (url, options) => {
|
||||
try {
|
||||
const r = await fetch(url, options)
|
||||
if (r.status >= 400) {
|
||||
throw Error(`request error with status ${r.status}`)
|
||||
}
|
||||
return await r.json()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return {
|
||||
succeed: false,
|
||||
message: e,
|
||||
data: null
|
||||
}
|
||||
}
|
||||
}
|
23
web/src/ajax/index.js
Normal file
23
web/src/ajax/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { ajax } from './ajax'
|
||||
|
||||
export default {
|
||||
getTitle: async () => {
|
||||
const j = await ajax('api/v1/name')
|
||||
return j.data
|
||||
},
|
||||
getInstruction: async () => {
|
||||
const j = await ajax('api/v1/instruction')
|
||||
return j.data
|
||||
},
|
||||
getICEUrl: async () => {
|
||||
const j = await ajax('api/v1/iceserver/url')
|
||||
return j.data
|
||||
},
|
||||
exchangeSDP: async offer => {
|
||||
const answer = ajax('api/v1/sdp', {
|
||||
method: "POST",
|
||||
body: JSON.stringify(offer)
|
||||
})
|
||||
return answer
|
||||
}
|
||||
}
|
188
web/src/components/AceScreen.vue
Normal file
188
web/src/components/AceScreen.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas id="vnc" />
|
||||
<div id="data">
|
||||
<video id="video" muted autoplay controls="none" />
|
||||
<audio id="audio" autoplay />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, defineProps } from "vue";
|
||||
import { store } from "../store";
|
||||
import ajax from "../ajax";
|
||||
|
||||
const props = defineProps(["methods"]);
|
||||
|
||||
const eventTypes = {
|
||||
mouseMove: 0,
|
||||
mouseButton: 1,
|
||||
keyboard: 2,
|
||||
control: 3,
|
||||
};
|
||||
|
||||
const controlEventTypes = {
|
||||
restart: 0,
|
||||
};
|
||||
|
||||
const makeEvent = (evType, args) => ({
|
||||
type: eventTypes[evType],
|
||||
args: args,
|
||||
});
|
||||
|
||||
let dataChannel;
|
||||
|
||||
// eslint-disable-next-line
|
||||
const sendSpecialKey = (key) => {
|
||||
console.log(key);
|
||||
|
||||
const specialKey = "ctrl-alt-" + key;
|
||||
dataChannel.send(
|
||||
JSON.stringify(
|
||||
makeEvent("keyboard", {
|
||||
key: specialKey,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
const resetVM = () => {
|
||||
dataChannel.send(
|
||||
JSON.stringify(
|
||||
makeEvent("control", {
|
||||
cmd: controlEventTypes.restart,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log(props.methods);
|
||||
// eslint-disable-next-line
|
||||
props.methods.sendSpecialKey = sendSpecialKey;
|
||||
// eslint-disable-next-line
|
||||
props.methods.resetVM = resetVM;
|
||||
|
||||
const video = document.querySelector("canvas#vnc");
|
||||
video.oncontextmenu = () => false;
|
||||
|
||||
store.getICEServers().then((servers) => {
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{
|
||||
urls: servers,
|
||||
},
|
||||
],
|
||||
});
|
||||
pc.oniceconnectionstatechange = () => console.log(pc.iceConnectionState);
|
||||
pc.addTransceiver("video", {
|
||||
direction: "recvonly",
|
||||
});
|
||||
pc.addTransceiver("audio", {
|
||||
direction: "recvonly",
|
||||
});
|
||||
|
||||
dataChannel = pc.createDataChannel("control");
|
||||
dataChannel.onmessage = (e) => {
|
||||
const enc = new TextDecoder("utf-8");
|
||||
const buf = new Uint8Array(e.data);
|
||||
const d = JSON.parse(enc.decode(buf));
|
||||
store.delay = +new Date() - d.server_time;
|
||||
store.qemuStatus = d.qemu_status;
|
||||
};
|
||||
pc.ontrack = (ev) => {
|
||||
console.log(ev);
|
||||
const el = document.querySelector(`${ev.track.kind}#${ev.track.kind}`);
|
||||
el.srcObject = ev.streams[0];
|
||||
el.autoplay = true;
|
||||
el.controls = false;
|
||||
el.oncontextmenu = () => false;
|
||||
document.getElementById("data").appendChild(el);
|
||||
|
||||
if (ev.track.kind === "video") {
|
||||
const ctx = video.getContext("2d");
|
||||
setTimeout(() => {
|
||||
ctx.drawImage(this.video, 0, 0);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
video.onmousemove = (ev) => {
|
||||
dataChannel.send(
|
||||
JSON.stringify(
|
||||
makeEvent("mouseMove", {
|
||||
dx: ev.clientX,
|
||||
dy: ev.clientY,
|
||||
dz: 0,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
video.onmousedown = (ev) => {
|
||||
dataChannel.send(
|
||||
JSON.stringify(
|
||||
makeEvent("mouseButton", {
|
||||
button: ev.button << 1,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
//video.onmousewheel = (ev) => {};
|
||||
window.onkeydown = (ev) => {
|
||||
let key = ev.key;
|
||||
if (ev.ctrlKey && ev.which !== 17) key = "ctrl-" + key;
|
||||
if (ev.shiftKey && ev.which !== 16) key = "shift-" + key;
|
||||
if (ev.altKey && ev.which !== 18) key = "alt-" + key;
|
||||
if (ev.metaKey && ev.which !== 91 && ev.which !== 93)
|
||||
key = "meta-" + key;
|
||||
if (!ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey)
|
||||
key = "0x" + ev.which.toString(16);
|
||||
|
||||
dataChannel.send(
|
||||
JSON.stringify(
|
||||
makeEvent("keyboard", {
|
||||
key: key,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pc.createOffer().then((offer) => {
|
||||
pc.setLocalDescription(offer);
|
||||
ajax
|
||||
.exchangeSDP(offer)
|
||||
.then((answer) =>
|
||||
pc.setRemoteDescription(new RTCSessionDescription(answer.data))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*
|
||||
video#video {
|
||||
vertical-align: middle;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
*/
|
||||
|
||||
canvas#vnc {
|
||||
vertical-align: middle;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
176
web/src/components/MainApp.vue
Normal file
176
web/src/components/MainApp.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div @contextmenu="noContextMenu">
|
||||
<n-watermark
|
||||
cross
|
||||
fullscreen
|
||||
v-if="show"
|
||||
:content="'ACE-' + title"
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="384"
|
||||
:height="384"
|
||||
:x-offset="12"
|
||||
:y-offset="60"
|
||||
:rotate="-15"
|
||||
></n-watermark>
|
||||
<div @mouseenter="menu" id="showMenu"></div>
|
||||
<div id="screenArea"><ace-screen :methods="screenController" /></div>
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<n-drawer :width="480" v-model:show="show" :native-scrollbar="true">
|
||||
<n-drawer-content :title="title">
|
||||
<n-collapse
|
||||
:default-expanded-names="['controlButtons', 'instructions']"
|
||||
>
|
||||
<n-collapse-item title="控制按钮" name="controlButtons">
|
||||
<n-grid :cols="1" :y-gap="8">
|
||||
<n-grid-item>
|
||||
<p>Ctrl+Alt+?</p>
|
||||
<n-button-group>
|
||||
<n-button type="error" strong @click="sendCtrlAltDelete">
|
||||
Delete
|
||||
</n-button>
|
||||
<n-button strong @click="sendCtrlAltF1">F1</n-button>
|
||||
<n-button strong @click="sendCtrlAltF2">F2</n-button>
|
||||
<n-button strong @click="sendCtrlAltF3">F3</n-button>
|
||||
<n-button strong @click="sendCtrlAltF4">F4</n-button>
|
||||
<n-button strong @click="sendCtrlAltF5">F5</n-button>
|
||||
<n-button strong @click="sendCtrlAltF6">F6</n-button>
|
||||
<n-button strong @click="sendCtrlAltF7">F7</n-button>
|
||||
</n-button-group>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-button type="error" strong @click="resetVM">
|
||||
重置
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<reload-icon />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-collapse-item>
|
||||
<n-collapse-item title="介绍" name="instructions">
|
||||
<n-card>
|
||||
<n-skeleton v-if="loading" text :repeat="10" />
|
||||
<n-skeleton v-if="loading" text style="width: 60%" />
|
||||
<div v-else v-html="renderMarkdown"></div>
|
||||
</n-card>
|
||||
</n-collapse-item>
|
||||
<n-collapse-item title="统计信息" name="stat">
|
||||
<n-card>
|
||||
<p>当前延迟:{{ store.delay }}ms</p>
|
||||
<p>云电脑状态:{{ store.qemuStatus }}</p>
|
||||
</n-card>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NWatermark,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NButtonGroup,
|
||||
NButton,
|
||||
NIcon,
|
||||
NGridItem,
|
||||
NGrid,
|
||||
//NSpace,
|
||||
useMessage,
|
||||
NCard,
|
||||
NSkeleton,
|
||||
} from "naive-ui";
|
||||
import { Reload as ReloadIcon } from "@vicons/ionicons5";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import AceScreen from "./AceScreen.vue";
|
||||
import { store } from "../store";
|
||||
import { marked } from "marked";
|
||||
|
||||
const show = ref(false);
|
||||
const title = ref("test123");
|
||||
const loading = ref(true);
|
||||
const menu = () => {
|
||||
show.value = true;
|
||||
};
|
||||
const noContextMenu = () => false;
|
||||
const message = useMessage();
|
||||
const instruction = ref("");
|
||||
const renderMarkdown = computed(() => {
|
||||
return marked.parse(
|
||||
instruction.value ? instruction.value : "# To be continued..."
|
||||
);
|
||||
});
|
||||
|
||||
const screenController = {};
|
||||
|
||||
const sendCtrlAltDelete = () => {
|
||||
sendSpecialKey("del");
|
||||
};
|
||||
|
||||
const sendCtrlAltF1 = () => {
|
||||
sendSpecialKey("f1");
|
||||
};
|
||||
|
||||
const sendCtrlAltF2 = () => {
|
||||
sendSpecialKey("f2");
|
||||
};
|
||||
|
||||
const sendCtrlAltF3 = () => {
|
||||
sendSpecialKey("f3");
|
||||
};
|
||||
|
||||
const sendCtrlAltF4 = () => {
|
||||
sendSpecialKey("f4");
|
||||
};
|
||||
|
||||
const sendCtrlAltF5 = () => {
|
||||
sendSpecialKey("f5");
|
||||
};
|
||||
|
||||
const sendCtrlAltF6 = () => {
|
||||
sendSpecialKey("f6");
|
||||
};
|
||||
|
||||
const sendCtrlAltF7 = () => {
|
||||
sendSpecialKey("f7");
|
||||
};
|
||||
|
||||
const sendSpecialKey = (key) => {
|
||||
screenController.sendSpecialKey(key);
|
||||
};
|
||||
|
||||
const resetVM = () => {
|
||||
screenController.resetVM();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
message.info("鼠标移动到屏幕右侧可打开控制面板");
|
||||
store.getTitle().then((t) => (title.value = t));
|
||||
store.loadInstruction().then((i) => {
|
||||
loading.value = false;
|
||||
instruction.value = i;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div#showMenu {
|
||||
float: right;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
width: 1px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
div#screenArea {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
zoom: 1;
|
||||
}
|
||||
</style>
|
4
web/src/main.js
Normal file
4
web/src/main.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
33
web/src/store/index.js
Normal file
33
web/src/store/index.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { reactive } from 'vue'
|
||||
import ajax from '../ajax'
|
||||
|
||||
export const store = reactive({
|
||||
title: '',
|
||||
iceServers: [],
|
||||
instruction: "",
|
||||
delay: 0,
|
||||
qemuStatus: "",
|
||||
|
||||
async loadTitle() {
|
||||
this.title = await ajax.getTitle()
|
||||
return this.title
|
||||
},
|
||||
async loadICEServers() {
|
||||
this.iceServers = await ajax.getICEUrl()
|
||||
return this.iceServers
|
||||
},
|
||||
async loadInstruction() {
|
||||
this.instruction = await ajax.getInstruction()
|
||||
return this.instruction
|
||||
},
|
||||
|
||||
async getTitle() {
|
||||
return this.title
|
||||
},
|
||||
async getICEServers() {
|
||||
return this.iceServers
|
||||
},
|
||||
async getInstruction() {
|
||||
return this.instruction
|
||||
}
|
||||
})
|
4
web/vue.config.js
Normal file
4
web/vue.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
6243
web/yarn.lock
Normal file
6243
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user