From 1b60f43df17b90d6f6c2f4c1a9c5186f1421b4d6 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 3 Nov 2024 11:44:40 +0100 Subject: [PATCH] add audio support, trying to manage MSD device --- Dockerfile | 18 +-- Dockerfile.ffmpeg-alpine | 4 +- cmd/kvm/main.go | 26 ++-- config/config.go | 40 ++++- docker-compose.yml | 34 +++-- entry.sh | 27 +++- go.mod | 6 +- go.sum | 16 +- hid.sh | 202 +++++++++++++++++++----- http/hw/hid/mouse.go | 4 +- http/hw/hid/op.go | 2 + http/hw/rtc/listener.go | 77 ++++++++++ http/hw/rtc/webrtc.go | 245 ++++++++++++------------------ http/hw/stream/audio.go | 18 ++- http/hw/stream/extprocess.go | 7 +- http/hw/stream/ffmpeg.go | 32 +++- http/hw/stream/pipedprocess.go | 270 +++++++++++++++++++++++++++++++++ http/hw/stream/video.go | 28 ++-- http/reqrsp/nanokvm.go | 17 +++ http/route/api.go | 147 +++++++++++++++++- http/route/nanokvm_ui.go | 90 +++++++++++ http/ws/ws.go | 12 ++ 22 files changed, 1059 insertions(+), 263 deletions(-) create mode 100644 http/hw/rtc/listener.go create mode 100644 http/hw/stream/pipedprocess.go create mode 100644 http/reqrsp/nanokvm.go create mode 100644 http/route/nanokvm_ui.go diff --git a/Dockerfile b/Dockerfile index 4b950db..7f5c2d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# currently where is no own frontend, so we have to take NanoKVM UI FROM node:20-alpine as frontend ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME/bin:$PATH" @@ -6,19 +7,18 @@ RUN apk add --update python3 make g++ git && rm -rf /var/cache/apk/* COPY ./NanoKVM/web /web RUN cd /web && pnpm install && pnpm build RUN mv /web/dist /static -#RUN git clone https://github.com/sipeed/nanokvm +#RUN git clone -b rkkvm https://github.com/argakon/nanokvm #RUN cd nanokvm/web && pnpm install && pnpm build #RUN mv nanokvm/web/dist /static - - FROM golang:alpine as backend -RUN apk add --no-cache git portaudio-dev +RUN apk add --no-cache git COPY . /app WORKDIR /app RUN go mod download RUN go build -o ./bin/ ./cmd/... +# we would use ffmpeg-rockchip from nyanmisaka with rockchip's mpp support FROM alpine as ffmpeg RUN apk add --no-cache \ autoconf \ @@ -37,13 +37,13 @@ RUN apk add --no-cache \ bash \ cmake \ git \ - libdrm-dev ninja-build meson v4l-utils-dev wget build-base bsd-compat-headers musl-dev + libdrm-dev ninja-build meson v4l-utils-dev wget build-base bsd-compat-headers musl-dev alsa-lib-dev libvpx-dev libopusenc-dev RUN mkdir -p ~/dev && cd ~/dev && \ git clone -b jellyfin-mpp --depth=1 https://github.com/nyanmisaka/mpp.git rkmpp && \ #git clone --depth=1 https://github.com/rockchip-linux/mpp.git rkmpp && \ cd rkmpp && mkdir rkmpp_build && cd rkmpp_build && \ - sed -i '/#include /a #include ' ~/dev/rkmpp/mpp/vproc/vdpp/test/hwpq_test.cpp && \ + sed -i '/#include /a #include ' ~/dev/rkmpp/mpp/vproc/vdpp/test/hwpq_test.cpp && \ cmake \ -DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_BUILD_TYPE=Release \ @@ -70,8 +70,7 @@ RUN mkdir -p ~/dev && cd ~/dev && \ RUN mkdir -p ~/dev && cd ~/dev && \ git clone --depth=1 https://github.com/nyanmisaka/ffmpeg-rockchip.git ffmpeg && cd ffmpeg && \ - ./configure --prefix=/usr --enable-indev=v4l2 --enable-gpl --enable-version3 --enable-libdrm --enable-rkmpp --enable-rkrga --enable-protocol=http --enable-protocol=tcp \ - #--enable-libx264 --enable-libvpx \ + ./configure --prefix=/usr --enable-indev=v4l2 --enable-gpl --enable-version3 --enable-libdrm --enable-rkmpp --enable-rkrga --enable-alsa --enable-libvpx --enable-libopus \ && \ make -j $(nproc) && make install @@ -80,11 +79,10 @@ FROM pikvm/ustreamer:latest as ustreamer FROM alpine RUN apk add --no-cache portaudio alsa-utils \ - libevent libjpeg-turbo libgpiod libbsd v4l-utils libdrm + libevent libjpeg-turbo libgpiod libbsd v4l-utils libdrm libvpx libopusenc COPY --from=frontend /static /app/static COPY --from=backend /app/bin/kvm /app/kvm -COPY --from=backend /app/bin/webrtc /app/webrtc COPY --from=ustreamer /ustreamer/ustreamer /app/ustreamer COPY --from=ffmpeg /usr/lib/librga.* /usr/lib COPY --from=ffmpeg /usr/lib/librockchip_mpp.* /usr/lib diff --git a/Dockerfile.ffmpeg-alpine b/Dockerfile.ffmpeg-alpine index 51ebf01..0aab358 100644 --- a/Dockerfile.ffmpeg-alpine +++ b/Dockerfile.ffmpeg-alpine @@ -16,7 +16,7 @@ RUN apk add --no-cache \ bash \ cmake \ git \ - libdrm-dev ninja-build meson v4l-utils-dev wget build-base bsd-compat-headers musl-dev + libdrm-dev ninja-build meson v4l-utils-dev wget build-base bsd-compat-headers musl-dev alsa-lib-dev RUN mkdir -p ~/dev && cd ~/dev && \ git clone -b jellyfin-mpp --depth=1 https://github.com/nyanmisaka/mpp.git rkmpp && \ @@ -49,7 +49,7 @@ RUN mkdir -p ~/dev && cd ~/dev && \ RUN mkdir -p ~/dev && cd ~/dev && \ git clone --depth=1 https://github.com/nyanmisaka/ffmpeg-rockchip.git ffmpeg && cd ffmpeg && \ - ./configure --prefix=/usr --enable-indev=v4l2 --enable-gpl --enable-version3 --enable-libdrm --enable-rkmpp --enable-rkrga --enable-protocol=http --enable-protocol=tcp \ + ./configure --prefix=/usr --enable-indev=v4l2 --enable-gpl --enable-version3 --enable-libdrm --enable-rkmpp --enable-rkrga --enable-protocol=http --enable-protocol=tcp --enable-alsa \ #--enable-libx264 --enable-libvpx \ && \ make -j $(nproc) && make install \ No newline at end of file diff --git a/cmd/kvm/main.go b/cmd/kvm/main.go index 3bc19ce..1bca581 100644 --- a/cmd/kvm/main.go +++ b/cmd/kvm/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "net/http" "rkkvm/config" "rkkvm/http/hw/rtc" "rkkvm/http/hw/stream" @@ -28,36 +27,31 @@ func main() { ustreamer.Start() //go ustreamer.Watch() } else if cfg.Stream.Source == config.StreamSourceH264 { - ffmpeg := stream.InitFFmpeg(cfg.FFmpeg.Path, cfg.FFmpeg.FormatArgs()) + ffmpeg := stream.InitFFmpeg(cfg.Video.Path, cfg.Video.FormatArgs()) ffmpeg.Start() - //go ffmpeg.Watch() + + audio := stream.InitPipedCmd(config.Get().Audio) + audio.Start() } else { log.Fatalf("unsupported stream source type: %v", cfg.Stream.Source) } - webrtc, err := rtc.Init(cfg.WebRtc.Host, cfg.WebRtc.Port) + webrtc, err := rtc.InitListener(cfg.WebRtc.Host, cfg.WebRtc.Port, 5006) if err != nil { log.Fatal(err) } - go webrtc.Read() - go webrtc.Listen() + defer webrtc.Close() + go webrtc.VideoListenerRead() + go webrtc.AudioListenerRead() r := gin.Default() r.Use(gin.Recovery()) - r.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) - }) route.Api(r) route.Static(r) if cfg.NanoKVMUISupport { - r.POST("/api/auth/login", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "token": "disabled", - }) - }) + route.Auth(r) + route.VM(r) } r.Run(fmt.Sprintf(":%d", cfg.Port)) diff --git a/config/config.go b/config/config.go index 1b3f02a..a7398e7 100644 --- a/config/config.go +++ b/config/config.go @@ -2,20 +2,29 @@ package config import ( "fmt" + "os" "strings" ) +var RootFS string + +func init() { + RootFS = os.Getenv("ROOT_FS") +} + type Config struct { LogLevel string `yaml:"log_level"` Auth bool `yaml:"auth"` AuthSecret string `yaml:"auth_secret"` Port int `yaml:"port"` UStreamer UStreamer `yaml:"ustreamer"` - FFmpeg FFmpeg `yaml:"ffmpeg"` + Video FFmpeg `yaml:"video"` + Audio []string `yaml:"audio"` WebRtc WebRtc `yaml:"webrtc"` Stream Stream `yaml:"stream"` - NanoKVMUISupport bool `yaml:"nano_kvm_ui_support"` + ISOPath string `yaml:"iso_path"` + NanoKVMUISupport bool `yaml:"nano_kvm_ui_support"` } type WebRtc struct { @@ -35,16 +44,20 @@ type Stream struct { const ( StreamSourceMjpeg = "mjpeg" StreamSourceH264 = "h264" + StreamSourceHevc = "hevc" ) type FFmpeg struct { ExtProcess - FPS int `yaml:"fps"` - Bitrate int `yaml:"bitrate"` + FPS int `yaml:"fps"` + Bitrate int `yaml:"bitrate"` + Height int `yaml:"height"` + GOP int `yaml:"gop"` + Codec string `yaml:"codec"` } func (f FFmpeg) FormatArgs() []string { - return strings.Split(fmt.Sprintf(f.Args, f.Bitrate*1000, f.FPS), " ") + return strings.Split(fmt.Sprintf(f.Args, f.Height, f.Codec, f.Bitrate*1000, f.FPS, f.GOP), " ") } type UStreamer struct { @@ -59,6 +72,8 @@ func Get() Config { return c } +// arecord -D hw:0,0 -f cd -r 44100 -c 2 | /app/ffmpeg -re -init_hw_device rkmpp=hw -filter_hw_device hw -f wav -i pipe:0 -f v4l2 -c:a aac -b:a 128k -ar 44100 -ac 2 -f rtp rtp://127.0.0.1:5006?pkt_size=1200 -i /dev/video0 -vf hwupload,scale_rkrga=h=720:force_original_aspect_ratio=1 -c:v h264_rkmpp -flags +low_delay -b:v 6000000 -framerate 60 -g 10 -f rtp rtp://127.0.0.1:5004?pkt_size=1200 + func Init() { c = Config{ LogLevel: "debug", @@ -72,21 +87,30 @@ func Init() { Args: "--host 0.0.0.0 --port 8888 -m BGR24 -f 60", }, }, - FFmpeg: FFmpeg{ + Video: FFmpeg{ ExtProcess: ExtProcess{ Path: "/app/ffmpeg", - Args: "-re -i /dev/video0 -c:v h264_rkmpp -b:v %d -framerate %d -bsf:v h264_mp4toannexb -g 10 -f rtp rtp://127.0.0.1:5004?pkt_size=1200", + Args: "-hide_banner -loglevel error -re -init_hw_device rkmpp=hw -filter_hw_device hw -i /dev/video0 -vf hwupload,scale_rkrga=h=%d:force_original_aspect_ratio=1 -c:v %s_rkmpp -flags +low_delay -b:v %d -framerate %d -g %d -f rtp rtp://127.0.0.1:5004?pkt_size=1200", + //Args: "-re -i /dev/video0 -c:v h264_rkmpp -b:v %d -framerate %d -bsf:v h264_mp4toannexb -g 10 -f rtp rtp://127.0.0.1:5004?pkt_size=1200", }, FPS: 60, Bitrate: 6000, + Height: 720, + GOP: 5, + Codec: "h264", + }, + Audio: []string{ + "/usr/bin/arecord -D hw:0,0 -f dat -r 48000 -c 2 --buffer-size=60", + "/app/ffmpeg -hide_banner -loglevel error -re -f wav -i pipe:0 -c:a libopus -b:a 48000 -sample_fmt s16 -ssrc 1 -payload_type 111 -f rtp -max_delay 0 -application lowdelay -f rtp rtp://127.0.0.1:5006?pkt_size=1200", }, WebRtc: WebRtc{ Host: "127.0.0.1", Port: 5004, }, Stream: Stream{ - Source: StreamSourceMjpeg, + Source: StreamSourceH264, }, + ISOPath: "/data", NanoKVMUISupport: true, } } diff --git a/docker-compose.yml b/docker-compose.yml index e3ae902..a7211b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,22 +4,32 @@ services: image: rkkvm:latest container_name: rkkvm privileged: true + pid: host volumes: + #- /dev/bus/usb:/dev/bus/usb - /sys/kernel/config:/sys/kernel/config - ./test_data:/data - devices: - - /dev/hidg0 - - /dev/hidg1 - - /dev/hidg2 - - /dev/video0 - - /dev/snd - - /dev/dri - - /dev/dma_heap - - /dev/mpp_service - - /dev/mali0 - - /dev/rga + - /dev:/dev + #devices: + #- /dev/bus/usb/001/001 + #- /dev/bus/usb/002/001 + #- /dev/bus/usb/003/001 + #- /dev/bus/usb/004/001 + #- /dev/bus/usb/005/001 + #- /dev/bus/usb/005/002 + #- /dev/bus/usb/006/001 + #- /dev/hidg0 + #- /dev/hidg1 + #- /dev/hidg2 + #- /dev/video0 + #- /dev/snd + #- /dev/dri + #- /dev/dma_heap + #- /dev/mpp_service + #- /dev/mali0 + #- /dev/rga #environment: # - EDID=1 ports: - 8888:8080 - restart: unless-stopped \ No newline at end of file + restart: always \ No newline at end of file diff --git a/entry.sh b/entry.sh index febdd37..52e5314 100644 --- a/entry.sh +++ b/entry.sh @@ -1,6 +1,10 @@ #!/bin/sh -set -e +#set -e +#export ROOT_FS=$(nsenter -t 1 -m docker inspect -f $'{{.GraphDriver.Data.MergedDir}}' rkkvm) +FS=$(nsenter -t 1 -m docker inspect -f $'{{.GraphDriver.Data.MergedDir}}' rkkvm) +#export LD_LIBRARY_PATH=$ROOT_FS/usr/lib:$ROOT_FS/lib +#export PATH=$ROOT_FS/bin:$ROOT_FS/sbin:$ROOT_FS/usr/bin:$ROOT_FS/usr/sbin:$PATH [ -n "$EDID" ] && { [ -n "$EDID_HEX" ] && echo "$EDID_HEX" > /edid.hex @@ -12,6 +16,23 @@ set -e } #/app/ustreamer --host 0.0.0.0 --port 8888 -m BGR24 -r 1280x720 -f 30 & +#nsenter -t 1 -m -u -i $ROOT_FS/hid.sh detach +#nsenter -t 1 -m -u -i $ROOT_FS/hid.sh delete +#nsenter -t 1 -m -u -i $ROOT_FS/hid.sh create +#nsenter -t 1 -m -u -i $ROOT_FS/hid.sh attach +#nsenter -t 1 -m -u -i $ROOT_FS/app/kvm $@ +#nsenter -t 1 -m -u -i $ROOT_FS/hid.sh detach +#nsenter -t 1 -m -u -i $ROOT_FS/hid.sh delete + + +#nsenter -t 1 -m -w/ -r/ /hid.sh detach +#nsenter -t 1 -m -w/ -r/ /hid.sh delete +#nsenter -t 1 -m -w/ -r/ /hid.sh create +#nsenter -t 1 -m -w/ -r/ /hid.sh attach +#nsenter -t 1 -m -w/ -r/ /app/kvm $@ +#nsenter -t 1 -m -w/ -r/ /hid.sh detach +#nsenter -t 1 -m -w/ -r/ /hid.sh delete + /hid.sh create -/app/kvm $@ -/hid.sh delete \ No newline at end of file +/hid.sh attach +/app/kvm $@ \ No newline at end of file diff --git a/go.mod b/go.mod index d8e6cbb..383ab30 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module rkkvm go 1.21 require ( + github.com/gin-contrib/static v1.1.2 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/pion/webrtc/v4 v4.0.1 github.com/sirupsen/logrus v1.9.3 ) @@ -16,12 +19,10 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-contrib/static v1.1.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -44,7 +45,6 @@ require ( github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect - github.com/pion/webrtc/v4 v4.0.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/wlynxg/anet v0.0.3 // indirect diff --git a/go.sum b/go.sum index f0ea0bd..e9c9f36 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,10 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -87,6 +91,8 @@ github.com/pion/webrtc/v4 v4.0.1 h1:6Unwc6JzoTsjxetcAIoWH81RUM4K5dBc1BbJGcF9WVE= github.com/pion/webrtc/v4 v4.0.1/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= 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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -110,32 +116,24 @@ github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguH golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/hid.sh b/hid.sh index df979ec..d680805 100755 --- a/hid.sh +++ b/hid.sh @@ -2,30 +2,31 @@ set -e GADGET_DIR=/sys/kernel/config/usb_gadget +GADGET_ID=g0 +MSD_ID=g1 -if [ ! -d "$GADGET_DIR" ]; then - echo "$DIRECTORY does not exist." - exit 0 -fi - -if [ "$1" = "create" ] -then - if [ -d "$GADGET_DIR/g0" ]; then +create() { + if [ -s "$GADGET_DIR/$GADGET_ID/UDC" ]; then echo "HID devices already exists" exit 0 fi - cd $GADGET_DIR && mkdir g0 && cd g0 + mkdir -p $GADGET_DIR/$GADGET_ID && \ + mkdir -p $GADGET_DIR/$MSD_ID && \ + cd $GADGET_DIR/$GADGET_ID - echo 0x3346 > idVendor - echo 0x1009 > idProduct + #echo 0x3346 > idVendor + echo 0x1b6d > idVendor + echo 0x0104 > idProduct + #echo 0x1009 > idProduct + echo 0x0300 > bcdUSB mkdir strings/0x409 - echo '0123456789ABCDEF' > strings/0x409/serialnumber + cat /proc/device-tree/serial-number > strings/0x409/serialnumber echo 'rockchip' > strings/0x409/manufacturer echo 'rk3588' > strings/0x409/product mkdir configs/c.1 - echo 0xE0 > configs/c.1/bmAttributes + #echo 0xE0 > configs/c.1/bmAttributes echo 120 > configs/c.1/MaxPower mkdir configs/c.1/strings/0x409 echo "rk3588" > configs/c.1/strings/0x409/configuration @@ -33,35 +34,168 @@ then # keyboard mkdir functions/hid.GS0 echo 1 > functions/hid.GS0/subclass - #echo 1 > functions/hid.GS0/wakeup_on_write echo 1 > functions/hid.GS0/protocol - echo 6 > functions/hid.GS0/report_length + #echo 6 > functions/hid.GS0/report_length + #echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.GS0/report_desc + echo 8 > functions/hid.GS0/report_length echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.GS0/report_desc ln -s functions/hid.GS0 configs/c.1 # mouse - mkdir functions/hid.GS1 - # echo 1 > functions/hid.GS1/subclass - #echo 1 > functions/hid.GS1/wakeup_on_write - echo 2 > functions/hid.GS1/protocol - echo -ne \\x34 > functions/hid.GS1/report_length - echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc - ln -s functions/hid.GS1 configs/c.1 + #mkdir functions/hid.GS1 + ## echo 1 > functions/hid.GS1/subclass + #echo 2 > functions/hid.GS1/protocol + #echo -ne \\x34 > functions/hid.GS1/report_length + #echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc + #ln -s functions/hid.GS1 configs/c.1 # touchpad - mkdir functions/hid.GS2 - # echo 1 > functions/hid.GS2/subclass - #echo 1 > functions/hid.GS2/wakeup_on_write - echo 2 > functions/hid.GS2/protocol - echo 6 > functions/hid.GS2/report_length - echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x03\\x15\\x00\\x25\\x01\\x95\\x03\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x05\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\x05\\x01\\x09\\x38\\x15\\x81\\x25\\x7f\\x35\\x00\\x45\\x00\\x75\\x08\\x95\\x01\\x81\\x06\\xc0\\xc0 > functions/hid.GS2/report_desc - ln -s functions/hid.GS2 configs/c.1 + #mkdir functions/hid.GS2 + ## echo 1 > functions/hid.GS2/subclass + #echo 2 > functions/hid.GS2/protocol + #echo 6 > functions/hid.GS2/report_length + #echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x03\\x15\\x00\\x25\\x01\\x95\\x03\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x05\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\x05\\x01\\x09\\x38\\x15\\x81\\x25\\x7f\\x35\\x00\\x45\\x00\\x75\\x08\\x95\\x01\\x81\\x06\\xc0\\xc0 > functions/hid.GS2/report_desc + #ln -s functions/hid.GS2 configs/c.1 - ls /sys/class/udc/ | cat > UDC + #cd $GADGET_DIR/$MSD_ID + + #echo 0x3346 > idVendor + #echo 0x1009 > idProduct + #mkdir strings/0x409 + #echo '0123456789ABCDEF' > strings/0x409/serialnumber + #echo 'rockchip' > strings/0x409/manufacturer + #echo 'rk3588_msd' > strings/0x409/product + + #mkdir configs/c.1 + #echo 0xE0 > configs/c.1/bmAttributes + #echo 250 > configs/c.1/MaxPower + #mkdir configs/c.1/strings/0x409 + #echo "rk3588_msd" > configs/c.1/strings/0x409/configuration + + # attach storage + #mkdir functions/mass_storage.disk0 + #ln -s functions/mass_storage.disk0 configs/c.1/ + #echo 1 > functions/mass_storage.disk0/lun.0/removable + #echo 1 > functions/mass_storage.disk0/lun.0/ro +} + +delete() { + if [ ! -d "$GADGET_DIR/$GADGET_ID" ]; then + echo "No HID devices to delete." + exit 0 + fi + + if grep -q '[^[:space:]]' "$GADGET_DIR/$GADGET_ID/UDC"; then + echo "HID devices are still attached. Please detach them before deleting." + exit 1 + fi + + cd "$GADGET_DIR" || exit 1 + + rm -f $GADGET_ID/configs/c.1/hid.GS0 + #rm -f $GADGET_ID/configs/c.1/hid.GS1 + #rm -f $GADGET_ID/configs/c.1/hid.GS2 + + rmdir $GADGET_ID/functions/hid.GS0 + #rmdir $GADGET_ID/functions/hid.GS1 + #rmdir $GADGET_ID/functions/hid.GS2 + + rmdir $GADGET_ID/configs/c.1/strings/0x409 + rmdir $GADGET_ID/configs/c.1 + rmdir $GADGET_ID/strings/0x409 + rmdir "$GADGET_ID" + + if grep -q '[^[:space:]]' "$GADGET_DIR/$MSD_ID/UDC"; then + echo "HID devices are still attached. Please detach them before deleting." + exit 1 + fi + + #rm -f $MSD_ID/configs/c.1/mass_storage.disk0 + #rmdir $MSD_ID/functions/mass_storage.disk0 + #rmdir $MSD_ID/configs/c.1/strings/0x409 + #rmdir $MSD_ID/configs/c.1 + #rmdir $MSD_ID/strings/0x409 + #rmdir "$MSD_ID" + + echo "HID devices have been deleted." +} + +attach() { + if grep -q '[^[:space:]]' "$GADGET_DIR/$GADGET_ID/UDC"; then + echo "Error: Device is already attached." + exit 1 + fi + + ls /sys/class/udc/ | cat > "$GADGET_DIR/$GADGET_ID/UDC" + #ls /sys/class/udc/ | cat > "$GADGET_DIR/$MSD_ID/UDC" + echo "Device has been attached." +} + +detach() { + echo '' > "$GADGET_DIR/$GADGET_ID/UDC" + #echo '' > "$GADGET_DIR/$MSD_ID/UDC" + echo "Device has been detached." +} + +mount_iso() { + iso_file="$1" + + if [[ ! -f "$iso_file" ]]; then + echo "Error: ISO file '$iso_file' does not exist." + exit 1 + fi + + if grep -q '[^[:space:]]' "$GADGET_DIR/$MSD_ID/UDC"; then + echo "Error: Device is still attached. Please detach it before mounting the ISO." + exit 1 + fi + + echo "$iso_file" > "$GADGET_DIR/$MSD_ID/functions/mass_storage.disk0/lun.0/file" + echo "ISO file: $iso_file inserted." +} + +mounted_iso() { + cat "$GADGET_DIR/$MSD_ID/functions/mass_storage.disk0/lun.0/file" +} + +unmount_iso() { + if grep -q '[^[:space:]]' "$GADGET_DIR/$MSD_ID/UDC"; then + echo "Error: Device is still attached. Please detach it before unmounting the ISO." + exit 1 + fi + + echo > "$GADGET_DIR/$MSD_ID/functions/mass_storage.disk0/lun.0/file" +} + +if [ ! -d "$GADGET_DIR" ]; then + echo "$DIRECTORY does not exist. Load modules or recompile the kernel to support usb_gadget" + exit 0 fi -if [ "$1" = "delete" ] -then - echo '' > $GADGET_DIR/g0/UDC -fi +case "$1" in + create) + create + ;; + delete) + delete + ;; + attach) + attach + ;; + detach) + detach + ;; + mount_iso) + mount_iso "$2" + ;; + unmount_iso) + unmount_iso + ;; + mounted_iso) + mounted_iso + ;; + *) + echo "Usage: $0 {create|delete|attach|detach|mount_iso|unmount_iso|mounted_iso}" + ;; +esac \ No newline at end of file diff --git a/http/hw/hid/mouse.go b/http/hw/hid/mouse.go index 9271ca1..8504781 100644 --- a/http/hw/hid/mouse.go +++ b/http/hw/hid/mouse.go @@ -87,10 +87,10 @@ func (h *Hid) writeWithTimeout(file *os.File, data []byte) { if err != nil { switch { case errors.Is(err, os.ErrClosed): - log.Debugf("hid already closed, reopen it...") + log.Tracef("hid already closed, reopen it...") h.OpenNoLock() case errors.Is(err, os.ErrDeadlineExceeded): - log.Debugf("write to hid timeout") + log.Tracef("write to hid timeout") default: log.Errorf("write to hid failed: %s", err) } diff --git a/http/hw/hid/op.go b/http/hw/hid/op.go index f4d5479..a150db0 100644 --- a/http/hw/hid/op.go +++ b/http/hw/hid/op.go @@ -8,6 +8,7 @@ import ( ) func (h *Hid) OpenNoLock() { + log.Debug("OpenNoLock hid") var err error h.CloseNoLock() @@ -39,6 +40,7 @@ func (h *Hid) Open() { } func (h *Hid) CloseNoLock() { + log.Debug("CloseNoLock") for _, file := range []*os.File{h.g0, h.g1, h.g2} { if file != nil { _ = file.Sync() diff --git a/http/hw/rtc/listener.go b/http/hw/rtc/listener.go new file mode 100644 index 0000000..7404901 --- /dev/null +++ b/http/hw/rtc/listener.go @@ -0,0 +1,77 @@ +package rtc + +import ( + "net" + "rkkvm/config" + + "github.com/pion/webrtc/v4" + log "github.com/sirupsen/logrus" +) + +func initUDPListener(host string, port int) (*net.UDPConn, error) { + l, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP(host), Port: port}) + if err != nil { + return nil, ErrWebRTCParam("failed to init webrtc listener: %v", err) + } + + // Increase the UDP receive buffer size + // Default UDP buffer sizes vary on different operating systems + bufferSize := 300000 // 300KB + err = l.SetReadBuffer(bufferSize) + if err != nil { + return nil, ErrWebRTCParam("failed to set read buffer: %v", err) + } + + return l, nil +} + +func InitListener(host string, port int, aPort int) (*RTC, error) { + vl, err := initUDPListener(host, port) + if err != nil { + return nil, err + } + + al, err := initUDPListener(host, aPort) + if err != nil { + return nil, err + } + + mimeType := "" + switch config.Get().Video.Codec { + case config.StreamSourceH264: + mimeType = webrtc.MimeTypeH264 + case config.StreamSourceHevc: // WebRTC currently has no official support for H265 + mimeType = webrtc.MimeTypeH265 + default: + return nil, ErrWebRTCParam("unknown video codec: %s", config.Get().Video.Codec) + } + + video, _ := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: mimeType}, "video", "rkkvm") + audio, _ := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "rkkvm") + rtc = &RTC{ + videoListener: vl, + audioListener: al, + peers: make(map[string]*webrtc.PeerConnection), + videoTrack: video, + audioTrack: audio, + } + + return rtc, nil +} + +func listenerRead(l *net.UDPConn, track *webrtc.TrackLocalStaticRTP) { + buf := make([]byte, 1600) // Buffer to hold incoming RTP packets + + for { + n, _, err := l.ReadFrom(buf) + if err != nil { + log.Errorf("error reading from UDP: %v\n", err) + continue + } + + _, err = track.Write(buf[:n]) + if err != nil { + log.Errorf("failed to send RTP to peer: %v", err) + } + } +} diff --git a/http/hw/rtc/webrtc.go b/http/hw/rtc/webrtc.go index be75a36..f318a33 100644 --- a/http/hw/rtc/webrtc.go +++ b/http/hw/rtc/webrtc.go @@ -1,184 +1,139 @@ package rtc import ( - "encoding/base64" - "encoding/json" "errors" "fmt" - "io" "net" + "sync" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/pion/webrtc/v4" ) -// https://github.com/pion/example-webrtc-applications/blob/master/sfu-ws/main.go - var rtc *RTC func Get() *RTC { return rtc } -var ErrPeerClosedConn = errors.New("webrtc: peer closed conn") +var ErrWebRTC = errors.New("webrtc") +var ErrWebRTCParam = func(format string, args ...any) error { + return fmt.Errorf("%w: "+format, args...) +} +var ErrPeerClosedConn = ErrWebRTCParam("peer closed conn") type RTC struct { - l *net.UDPConn - peer *webrtc.PeerConnection - track *webrtc.TrackLocalStaticRTP - sender *webrtc.RTPSender - localSession string + peers map[string]*webrtc.PeerConnection + videoListener *net.UDPConn + audioListener *net.UDPConn + videoTrack *webrtc.TrackLocalStaticRTP + audioTrack *webrtc.TrackLocalStaticRTP + m sync.Mutex +} + +func (r *RTC) AddPeer(p *webrtc.PeerConnection, offer webrtc.SessionDescription) (*webrtc.SessionDescription, error) { + peerID := uuid.New().String() + r.m.Lock() + r.peers[peerID] = p + r.m.Unlock() + + p.OnConnectionStateChange(func(connState webrtc.PeerConnectionState) { + if connState == webrtc.PeerConnectionStateFailed || connState == webrtc.PeerConnectionStateClosed { + r.m.Lock() + defer r.m.Unlock() + delete(r.peers, peerID) + p.Close() + + peers := make([]string, 0, len(r.peers)) + for p := range r.peers { + peers = append(peers, p) + } + log.WithField("peers", peers).Infof("Peer %s disconnected and resources cleaned up.", peerID) + } + }) + + vSender, err := p.AddTrack(r.videoTrack) + if err != nil { + return nil, ErrWebRTCParam("failed to add video track: %v", err) + } + processRTCP(vSender) + aSender, err := p.AddTrack(r.audioTrack) + if err != nil { + return nil, ErrWebRTCParam("failed to add audio track: %v", err) + } + processRTCP(aSender) + + if err := p.SetRemoteDescription(offer); err != nil { + return nil, ErrWebRTCParam("failed to set remote description: %v", err) + } + + answer, err := p.CreateAnswer(nil) + if err != nil { + return nil, ErrWebRTCParam("failed to create answer: %v", err) + } + gatherComplete := webrtc.GatheringCompletePromise(p) + + if err := p.SetLocalDescription(answer); err != nil { + return nil, ErrWebRTCParam("failed to set local description: %v", err) + } + <-gatherComplete + + return p.LocalDescription(), nil +} + +func (r *RTC) VideoListenerRead() { + listenerRead(r.videoListener, r.videoTrack) +} + +func (r *RTC) AudioListenerRead() { + listenerRead(r.audioListener, r.audioTrack) } func (r *RTC) Close() error { - return r.l.Close() + r.videoListener.Close() + r.audioListener.Close() + + return nil } -// Read incoming RTCP packets -// Before these packets are returned they are processed by interceptors. For things -// like NACK this needs to be called. -func (r *RTC) Read() { - rtcpBuf := make([]byte, 1500) - for { - if _, _, rtcpErr := r.sender.Read(rtcpBuf); rtcpErr != nil { - log.Errorf("failed to read RTCP packet: %v", rtcpErr) - return - } - } -} - -func Init(host string, port int) (*RTC, error) { - peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ +func NewPeer() (*webrtc.PeerConnection, error) { + peer, err := webrtc.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) - if err != nil { - return nil, fmt.Errorf("failed to create peer connection: %v", err) + if err == nil { + // Set the handler for ICE connection state + // This will notify you when the peer has connected/disconnected + peer.OnICEConnectionStateChange(func(connState webrtc.ICEConnectionState) { + log.Infof("Connection State has changed %s", connState.String()) + + if connState == webrtc.ICEConnectionStateFailed { + if closeErr := peer.Close(); closeErr != nil { + panic(closeErr) + } + } + }) } - // Open a UDP Listener for RTP Packets on port 5004 - l, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP(host), Port: port}) - if err != nil { - return nil, fmt.Errorf("failed to init webrtc listener: %v", err) - } - - // Increase the UDP receive buffer size - // Default UDP buffer sizes vary on different operating systems - bufferSize := 300000 // 300KB - err = l.SetReadBuffer(bufferSize) - if err != nil { - return nil, fmt.Errorf("failed to set read buffer: %v", err) - } - - track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "pion") - if err != nil { // it should never happens - panic(fmt.Sprintf("failed to create video track: %v", err)) - } - - rtpSender, err := peerConnection.AddTrack(track) - if err != nil { - return nil, fmt.Errorf("failed to add track to peer connection: %v", err) - } - - r := &RTC{ - peer: peerConnection, - sender: rtpSender, - track: track, - l: l, - } - rtc = r - - return r, nil + return peer, err } -func (r *RTC) Handshake(clientSession string) (string, error) { - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - r.peer.OnICEConnectionStateChange(func(connState webrtc.ICEConnectionState) { - log.Infof("Connection State has changed %s", connState.String()) +// Read incoming RTCP packets +// Before these packets are retuned they are processed by interceptors. For things +// like NACK this needs to be called. +func processRTCP(rtpSender *webrtc.RTPSender) { + go func() { + rtcpBuf := make([]byte, 1500) - if connState == webrtc.ICEConnectionStateFailed { - if closeErr := r.peer.Close(); closeErr != nil { - panic(closeErr) + for { + if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { + return } } - }) - - // Wait for the offer to be pasted - offer := webrtc.SessionDescription{} - decode(clientSession, &offer) - fmt.Printf("Offer: %+v\n", offer) - - // Set the remote SessionDescription - if err := r.peer.SetRemoteDescription(offer); err != nil { - return "", fmt.Errorf("failed to set remote session description: %v", err) - } - - // Create answer - answer, err := r.peer.CreateAnswer(nil) - if err != nil { - return "", fmt.Errorf("failed to create answer: %v", err) - } - - // Create channel that is blocked until ICE Gathering is complete - gatherComplete := webrtc.GatheringCompletePromise(r.peer) - - // Sets the LocalDescription, and starts our UDP listeners - if err = r.peer.SetLocalDescription(answer); err != nil { - return "", fmt.Errorf("failed to set local description: %v", err) - } - - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - <-gatherComplete - - r.localSession = encode(r.peer.LocalDescription()) - return r.localSession, nil -} - -func (r *RTC) Listen() error { - // Read RTP packets forever and send them to the WebRTC Client - inboundRTPPacket := make([]byte, 1600) // UDP MTU - for { - n, _, err := r.l.ReadFrom(inboundRTPPacket) - if err != nil { - return fmt.Errorf("error during read: %v", err) - } - - if _, err = r.track.Write(inboundRTPPacket[:n]); err != nil { - if errors.Is(err, io.ErrClosedPipe) { - // The peerConnection has been closed. - return ErrPeerClosedConn - } - - return fmt.Errorf("failed to send RTP packet to client: %v", err) - } - } -} - -// JSON encode + base64 a SessionDescription -func encode(obj *webrtc.SessionDescription) string { - b, err := json.Marshal(obj) - if err != nil { - panic(err) - } - - return base64.StdEncoding.EncodeToString(b) -} - -// Decode a base64 and unmarshal JSON into a SessionDescription -func decode(in string, obj *webrtc.SessionDescription) { - b, err := base64.StdEncoding.DecodeString(in) - if err != nil { - panic(err) - } - - if err = json.Unmarshal(b, obj); err != nil { - panic(err) - } + }() } diff --git a/http/hw/stream/audio.go b/http/hw/stream/audio.go index 2c67d4f..1269f6e 100644 --- a/http/hw/stream/audio.go +++ b/http/hw/stream/audio.go @@ -1,33 +1,47 @@ package stream import ( - "github.com/gin-gonic/gin" "io" "net/http" "os/exec" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) +type Audio struct { +} + func AudioHandler(c *gin.Context) { cmd := exec.Command("ffmpeg", "-f", "alsa", "-i", "hw:0,0", "-acodec", "aac", "-f", "mp4", "-") + + //c := "arecord -D hw:0,0 -f cd -r 44100 -c 2 | /app/ffmpeg -re -f wav -i pipe:0 -c:a aac -b:a 128k -ar 44100 -ac 2 -f rtp rtp://127.0.0.1:5006?pkt_size=1200" + //cmd := exec.Command("sh", "-c", c) // ffmpeg -f alsa -i hw:0,0 -acodec aac -f mp4 - stdout, err := cmd.StdoutPipe() if err != nil { c.String(http.StatusInternalServerError, "Failed to capture audio: %v", err) + log.Errorf("Failed to capture audio: %v", err) return } if err := cmd.Start(); err != nil { c.String(http.StatusInternalServerError, "Failed to start ffmpeg: %v", err) + log.Errorf("Failed to start ffmpeg: %v", err) return } defer cmd.Wait() - c.Header("Content-Type", "audio/mp4") + //c.Header("Content-Type", "audio/aac") + c.Header("Content-Type", "audio/wav") + c.Header("Transfer-Encoding", "chunked") + if _, err := io.Copy(c.Writer, stdout); err != nil { c.String(http.StatusInternalServerError, "Failed to stream audio: %v", err) + log.Errorf("Failed to stream audio: %v", err) return } } diff --git a/http/hw/stream/extprocess.go b/http/hw/stream/extprocess.go index 92347c3..83df277 100644 --- a/http/hw/stream/extprocess.go +++ b/http/hw/stream/extprocess.go @@ -4,6 +4,7 @@ import ( "io" "os" "os/exec" + "rkkvm/config" "sync" log "github.com/sirupsen/logrus" @@ -25,7 +26,7 @@ type ExtProcess struct { func Init(path string, args []string) *ExtProcess { return &ExtProcess{ args: args, - path: path, + path: config.RootFS + path, } } @@ -41,7 +42,7 @@ func (u *ExtProcess) Start() { u.stopChan = make(chan struct{}) u.finished = make(chan struct{}) - log.Debug("Starting external process...") + log.Debugf("Starting external process: %s %v", u.path, u.args) u.cmd = exec.Command(u.path, u.args...) u.cmd.Stdout = os.Stdout u.cmd.Stderr = os.Stderr @@ -99,8 +100,6 @@ func (u *ExtProcess) Stop() { log.Errorf("Failed to close stdin: %v", err) } - log.Info("waiting for finish") - select { case <-u.finished: log.Info("stopped as expected") diff --git a/http/hw/stream/ffmpeg.go b/http/hw/stream/ffmpeg.go index d3f8734..d5bf413 100644 --- a/http/hw/stream/ffmpeg.go +++ b/http/hw/stream/ffmpeg.go @@ -13,6 +13,18 @@ import ( // vp8 //ffmpeg -re -i /dev/video0 -c:v libvpx -crf 10 -b:v 3M -deadline 1 -g 10 -error-resilient 1 -auto-alt-ref 1 -an -f rtp rtp://127.0.0.1:5004?pkt_size=1200 +/* +arecord -D hw:0,0 -f cd -r 44100 -c 2 | /app/ffmpeg -re -init_hw_device rkmpp=hw -filter_hw_device hw \ +-f wav -thread_queue_size 4096 -i pipe:0 -f v4l2 -i /dev/video0 \ +-vf hwupload,scale_rkrga=h=720:force_original_aspect_ratio=1 \ +-c:v h264_rkmpp -flags +low_delay -b:v 6000000 -framerate 60 -g 10 \ +-map 0:a -c:a aac -b:a 128k -ar 44100 -ac 2 -f rtp rtp://127.0.0.1:5006?pkt_size=1200 \ +-map 1:v -f rtp rtp://127.0.0.1:5004?pkt_size=1200 + + +arecord -D hw:0,0 -f cd -r 44100 -c 2 | /app/ffmpeg -re -f wav -i pipe:0 -c:a aac -b:a 128k -ar 44100 -ac 2 -f rtp rtp://127.0.0.1:5006?pkt_size=1200 +*/ + // https://jsfiddle.net/z7ms3u5r/ var ffmpeg *FFmpeg @@ -40,10 +52,28 @@ func (f *FFmpeg) SetFPS(fps int) { f.ChangeArgs(f.FormatArgs()) } +func (f *FFmpeg) SetResolution(height int) { + f.Height = height + if f.Height <= 0 { + f.Height = 1080 + } + + f.ChangeArgs(f.FormatArgs()) +} + +func (f *FFmpeg) SetGOP(gop int) { + f.GOP = gop + if f.GOP < 0 { + f.GOP = 0 + } + + f.ChangeArgs(f.FormatArgs()) +} + func InitFFmpeg(path string, args []string) *FFmpeg { ffmpeg = &FFmpeg{ ExtProcess: Init(path, args), - FFmpeg: config.Get().FFmpeg, + FFmpeg: config.Get().Video, } return ffmpeg } diff --git a/http/hw/stream/pipedprocess.go b/http/hw/stream/pipedprocess.go new file mode 100644 index 0000000..3b58206 --- /dev/null +++ b/http/hw/stream/pipedprocess.go @@ -0,0 +1,270 @@ +package stream + +import ( + "log" + "os/exec" + "rkkvm/config" + "strings" + "sync" + "syscall" +) + +var pipedCmd *PipedCmd + +// PipedCmd struct manages a sequence of piped commands. +type PipedCmd struct { + cmds []*exec.Cmd + mu sync.Mutex + running bool + finished chan struct{} +} + +// InitPipedCmd initializes a PipedCmd instance with a sequence of commands. +func InitPipedCmd(cmds []string) *PipedCmd { + pipedCmds := make([]*exec.Cmd, len(cmds)) + + // Initialize each command in the sequence + for i, cmd := range cmds { + if len(cmd) == 0 { + continue + } + cmdArgs := strings.Split(cmd, " ") + pipedCmds[i] = exec.Command(config.RootFS+"/"+cmdArgs[0], cmdArgs[1:]...) + } + + pipedCmd = &PipedCmd{ + cmds: pipedCmds, + finished: make(chan struct{}), + } + return pipedCmd +} + +// Start begins execution of all commands in the piped sequence. +func (p *PipedCmd) Start() error { + p.mu.Lock() + defer p.mu.Unlock() + + if p.running { + log.Println("Process is already running.") + return nil + } + + // Pipe each command's output to the next command's input + for i := 0; i < len(p.cmds)-1; i++ { + stdout, err := p.cmds[i].StdoutPipe() + if err != nil { + return err + } + p.cmds[i+1].Stdin = stdout + } + + // Start each command in the sequence + for _, cmd := range p.cmds { + if err := cmd.Start(); err != nil { + p.terminateAll() // Clean up if any command fails to start + return err + } + } + + p.running = true + + // Monitor commands in a goroutine to handle failures + go p.monitorCommands() + + return nil +} + +// monitorCommands waits for each command to finish and checks for errors. +func (p *PipedCmd) monitorCommands() { + var wg sync.WaitGroup + wg.Add(len(p.cmds)) + + for _, cmd := range p.cmds { + go func(cmd *exec.Cmd) { + defer wg.Done() + err := cmd.Wait() + if err != nil { + log.Printf("Command failed: %v", err) + p.terminateAll() // Terminate all if any command fails + } + }(cmd) + } + + // Wait for all commands to complete or terminate + wg.Wait() + p.mu.Lock() + p.running = false + close(p.finished) + p.mu.Unlock() +} + +// terminateAll sends a termination signal to all running commands. +func (p *PipedCmd) terminateAll() { + p.mu.Lock() + defer p.mu.Unlock() + + for _, cmd := range p.cmds { + if cmd.Process != nil { + _ = cmd.Process.Signal(syscall.SIGTERM) // Send SIGTERM to allow graceful termination + } + } +} + +// Stop manually stops all commands in the sequence. +func (p *PipedCmd) Stop() { + p.mu.Lock() + defer p.mu.Unlock() + + if !p.running { + log.Println("Process is not running.") + return + } + + log.Println("Stopping process...") + p.terminateAll() + p.running = false + close(p.finished) +} + +/* +import ( + "os" + "os/exec" + "strings" + "sync" + + log "github.com/sirupsen/logrus" +) + +type PipedCmd struct { + cmds []string + cmdsExec []*exec.Cmd + mu sync.Mutex + running bool + stopChan chan struct{} + finished chan struct{} +} + +// Init initializes the PipedCmd with a slice of command strings +func InitPipedCmd(cmds []string) *PipedCmd { + return &PipedCmd{ + cmds: cmds, + } +} + +// Start initializes and starts the piped commands +func (p *PipedCmd) Start() { + p.mu.Lock() + defer p.mu.Unlock() + + if p.running { + log.Debug("process is already running.") + return + } + + p.stopChan = make(chan struct{}) + p.finished = make(chan struct{}) + + log.Debugf("Starting piped commands: <%s>", strings.Join(p.cmds, " | ")) + + // Create commands and set up pipes + for i, cmdStr := range p.cmds { + // Split command string into command and arguments + cmdParts := strings.Fields(cmdStr) + if len(cmdParts) == 0 { + log.Errorf("Empty command string at index %d", i) + continue + } + + cmd := exec.Command(cmdParts[0], cmdParts[1:]...) + + // Set up pipes for stdin/stdout + if i > 0 { + stdin, err := p.cmdsExec[i-1].StdoutPipe() + if err != nil { + log.Errorf("Couldn't set up stdout pipe for command %s: %v", p.cmdsExec[i-1].Path, err) + return + } + cmd.Stdin = stdin + } + + cmd.Stderr = os.Stderr // Log stderr to standard error output + + p.cmdsExec = append(p.cmdsExec, cmd) + } + + // Start the first command + if err := p.cmdsExec[0].Start(); err != nil { + log.Errorf("Failed to start command: %v", err) + return + } + p.running = true + + // Start remaining commands + for _, cmd := range p.cmdsExec[1:] { + if err := cmd.Start(); err != nil { + log.Errorf("Failed to start command: %v", err) + return + } + } + + go func() { + // Wait for the last command to finish + err := p.cmdsExec[len(p.cmdsExec)-1].Wait() + p.running = false + log.Errorf("process exited with error: %v", err) + + // Signal that the process has finished + close(p.finished) + close(p.stopChan) + }() +} + +// Stop terminates the piped commands gracefully +func (p *PipedCmd) Stop() { + p.mu.Lock() + defer p.mu.Unlock() + + if !p.running { + log.Debug("process is not running.") + return + } + + log.Warn("Stopping process...") + + for _, cmd := range p.cmdsExec { + if err := cmd.Process.Kill(); err != nil { + log.Errorf("Failed to kill process: %v", err) + } + } + + select { + case <-p.finished: + log.Info("stopped as expected") + case <-p.stopChan: + log.Info("was killed") + } + + p.running = false +} + +// ChangeArgs updates the command arguments +func (p *PipedCmd) ChangeArgs(newCmds []string) { + p.mu.Lock() + defer p.mu.Unlock() + + p.cmds = newCmds + log.Printf("Updated process commands: %v", p.cmds) +} + +// Watch monitors the process and restarts if it stops unexpectedly +func (p *PipedCmd) Watch() { + for { + select { + case <-p.stopChan: + log.Errorf("process stopped unexpectedly. Restarting...") + p.Start() + } + } +} +*/ diff --git a/http/hw/stream/video.go b/http/hw/stream/video.go index 58d88c9..1494e05 100644 --- a/http/hw/stream/video.go +++ b/http/hw/stream/video.go @@ -7,9 +7,11 @@ import ( "net/http" "rkkvm/config" "rkkvm/http/hw/rtc" + "rkkvm/http/reqrsp" "strconv" "github.com/gin-gonic/gin" + "github.com/pion/webrtc/v4" log "github.com/sirupsen/logrus" ) @@ -136,23 +138,29 @@ func MjpegHandler(c *gin.Context) { } } -func WebRTCHandshake(c *gin.Context) { - str, err := c.GetRawData() - if err != nil { - c.String(http.StatusBadRequest, err.Error()) +func WebRTCPeerConnect(c *gin.Context) { + var offer webrtc.SessionDescription + if err := c.ShouldBindJSON(&offer); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid offer"}) return } - log.Debugf("Client session description: %s", string(str)) - - r := rtc.Get() - localSession, err := r.Handshake(string(str)) + peer, err := rtc.NewPeer() if err != nil { - c.String(http.StatusBadRequest, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create peer connection"}) return } - c.String(http.StatusOK, localSession) + answer, err := rtc.Get().AddPeer(peer, offer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add peer"}) + return + } + + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + Data: answer, + }) } func WebRTCSettings(c *gin.Context) { diff --git a/http/reqrsp/nanokvm.go b/http/reqrsp/nanokvm.go new file mode 100644 index 0000000..0c1b122 --- /dev/null +++ b/http/reqrsp/nanokvm.go @@ -0,0 +1,17 @@ +package reqrsp + +const MsgSuccess = "success" + +type NanoKVMRsp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data any `json:"data"` +} + +type FilesRsp struct { + Files []string `json:"files"` +} + +type FileRsp struct { + File string `json:"file"` +} diff --git a/http/route/api.go b/http/route/api.go index 1514671..c1839db 100644 --- a/http/route/api.go +++ b/http/route/api.go @@ -1,11 +1,20 @@ package route import ( + "net/http" + "os" + "os/exec" + "path/filepath" + "rkkvm/config" + "rkkvm/http/hw/hid" "rkkvm/http/hw/stream" "rkkvm/http/middleware" + "rkkvm/http/reqrsp" "rkkvm/http/ws" + "strings" "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) func Api(e *gin.Engine) { @@ -15,7 +24,141 @@ func Api(e *gin.Engine) { api.GET("/stream/audio", stream.AudioHandler) api.GET("/ws", ws.ConnHandler) - api.POST("/stream/webrtc", stream.WebRTCHandshake) - + api.POST("/stream/webrtc", stream.WebRTCPeerConnect) api.GET("/stream/webrtc", stream.WebRTCSettings) + + api.POST("/storage/image", uploadHandler) + api.GET("/storage/image", listHandler) + api.GET("/storage/image/mounted", mountedHandler) + api.POST("/storage/image/mount", mountHandler) + api.POST("/storage/image/unmount", unmountHandler) +} + +func uploadHandler(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to get file"}) + return + } + + filePath := filepath.Join(config.Get().ISOPath, file.Filename) + if err := c.SaveUploadedFile(file, filePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "File uploaded successfully"}) +} + +func listHandler(c *gin.Context) { + files, err := os.ReadDir(config.Get().ISOPath) + if err != nil { + c.JSON(http.StatusInternalServerError, reqrsp.NanoKVMRsp{ + Code: -2, + Msg: "get images failed", + }) + return + } + + var fileNames []string + for _, f := range files { + if !f.IsDir() { + fname := strings.ToLower(f.Name()) + if strings.HasSuffix(fname, ".iso") || strings.HasSuffix(fname, ".img") { + fileNames = append(fileNames, f.Name()) + } + } + } + + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + Data: reqrsp.FilesRsp{ + Files: fileNames, + }, + }) +} + +func mountHandler(c *gin.Context) { + var request struct { + Filename string `json:"file"` + } + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, reqrsp.NanoKVMRsp{ + Code: -1, + Msg: "invalid arguments", + }) + return + } + + imageFile := filepath.Join(config.Get().ISOPath, request.Filename) + _, err := os.Stat(imageFile) + if os.IsNotExist(err) { + c.JSON(http.StatusInternalServerError, reqrsp.NanoKVMRsp{ + Code: -2, + Msg: "file not exists: " + request.Filename, + }) + return + } + + hid.GetHid().Close() + + cmds := []string{ + config.RootFS + "/hid.sh detach", + config.RootFS + "/hid.sh mount_iso " + imageFile, //+ strconv.Quote(imageFile), + config.RootFS + "/hid.sh attach", + } + + for _, cmd := range cmds { + log.Debugf("Executing: %s", cmd) + cc := exec.Command("sh", "-c", cmd) + cc.Stdout = os.Stdout + cc.Stderr = os.Stderr + if err := cc.Run(); err != nil { + c.JSON(http.StatusInternalServerError, reqrsp.NanoKVMRsp{ + Code: -2, + Msg: "execute command failed: " + cmd, + }) + return + } + } + + hid.GetHid().Open() + + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + }) + + os.Exit(0) +} + +func unmountHandler(c *gin.Context) { + if output, err := exec.Command(config.RootFS + "/hid.sh unmount_iso").Output(); err != nil { + c.JSON(http.StatusInternalServerError, reqrsp.NanoKVMRsp{ + Code: -2, + Msg: "execute command failed: " + string(output), + }) + } + + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + }) +} + +func mountedHandler(c *gin.Context) { + cmd := exec.Command(config.RootFS+"/hid.sh", "mounted_iso") + output, err := cmd.Output() + if err != nil { + c.JSON(http.StatusInternalServerError, reqrsp.NanoKVMRsp{ + Code: -2, + Msg: "read failed", + }) + return + } + + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + Data: reqrsp.FileRsp{ + File: string(output), + }, + }) } diff --git a/http/route/nanokvm_ui.go b/http/route/nanokvm_ui.go new file mode 100644 index 0000000..ba2e8e7 --- /dev/null +++ b/http/route/nanokvm_ui.go @@ -0,0 +1,90 @@ +package route + +import ( + "net/http" + "rkkvm/http/hw/stream" + "rkkvm/http/middleware" + "rkkvm/http/reqrsp" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +type LoginReq struct { + Username string `validate:"required"` + Password string `validate:"required"` +} + +type LoginRsp struct { + Token string `json:"token"` +} + +type ChangePasswordReq struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` +} + +func Auth(r *gin.Engine) { + r.POST("/api/auth/login", login) + + api := r.Group("/api").Use(middleware.CheckToken()) + + api.POST("/auth/password", func(ctx *gin.Context) {}) +} + +func VM(r *gin.Engine) { + api := r.Group("/api").Use(middleware.CheckToken()) + + api.POST("/vm/screen", SetScreen) + api.GET("/vm/gpio", func(ctx *gin.Context) {}) // just to not space in front log +} + +type SetScreenReq struct { + Type string `validate:"required"` // resolution / fps / quality + Value int `validate:"number"` // value +} + +func SetScreen(c *gin.Context) { + var req SetScreenReq + + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, reqrsp.NanoKVMRsp{ + Code: -1, + Msg: "invalid arguments", + }) + return + } + + ffmpeg := stream.GetFFmpeg() + switch req.Type { + case "fps": + ffmpeg.SetFPS(req.Value) + case "quality": + ffmpeg.SetBitrate(req.Value * 100) + case "resolution": + ffmpeg.SetResolution(req.Value) + default: + c.JSON(http.StatusBadRequest, reqrsp.NanoKVMRsp{ + Code: -2, + Msg: "invalid type", + }) + return + } + ffmpeg.Stop() + ffmpeg.Start() + + log.Debugf("update screen: %+v", req) + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + }) +} + +// FIXME: auth disabled while backend doesn't have key features +func login(c *gin.Context) { + c.JSON(http.StatusOK, reqrsp.NanoKVMRsp{ + Msg: reqrsp.MsgSuccess, + Data: gin.H{ + "token": "disabled", + }, + }) +} diff --git a/http/ws/ws.go b/http/ws/ws.go index 60c26c0..548624e 100644 --- a/http/ws/ws.go +++ b/http/ws/ws.go @@ -2,6 +2,7 @@ package ws import ( "encoding/json" + "net" "net/http" "rkkvm/config" "rkkvm/http/hw/hid" @@ -47,6 +48,17 @@ func ConnHandler(c *gin.Context) { } log.Debugf("websocket connected") + // Get the underlying net.Conn from the WebSocket connection + netConn := conn.UnderlyingConn() + + // Enable TCP_NODELAY + if tcpConn, ok := netConn.(*net.TCPConn); ok { + if err := tcpConn.SetNoDelay(true); err != nil { + log.Println("Failed to set TCP_NODELAY:", err) + return + } + } + cl := &client{ hid: hid.GetHid(), conn: conn,