Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions audio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kvm

import (
"os/exec"
)

func runAudioClient() (cmd *exec.Cmd, err error) {
return startNativeBinary("/userdata/jetkvm/bin/jetkvm_audio")
}

func StartAudioServer() {
nativeAudioSocketListener = StartNativeSocketServer("/var/run/jetkvm_audio.sock", handleAudioClient, false)
nativeLogger.Debug().Msg("native app audio sock started")
}
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ var defaultConfig = &Config{
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
Audio: true,
},
NetworkConfig: &network.NetworkConfig{},
DefaultLogLevel: "INFO",
Expand Down
19 changes: 19 additions & 0 deletions internal/usbgadget/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
// mass storage
"mass_storage_base": massStorageBaseConfig,
"mass_storage_lun0": massStorageLun0Config,
// audio
"audio": {
order: 4000,
device: "uac1.usb0",
path: []string{"functions", "uac1.usb0"},
configPath: []string{"uac1.usb0"},
attrs: gadgetAttributes{
"p_chmask": "3",
"p_srate": "48000",
"p_ssize": "2",
"p_volume_present": "0",
"c_chmask": "3",
"c_srate": "48000",
"c_ssize": "2",
"c_volume_present": "0",
},
},
}

func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
Expand All @@ -73,6 +90,8 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
return u.enabledDevices.MassStorage
case "mass_storage_lun0":
return u.enabledDevices.MassStorage
case "audio":
return u.enabledDevices.Audio
default:
return true
}
Expand Down
1 change: 1 addition & 0 deletions internal/usbgadget/usbgadget.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Devices struct {
RelativeMouse bool `json:"relative_mouse"`
Keyboard bool `json:"keyboard"`
MassStorage bool `json:"mass_storage"`
Audio bool `json:"audio"`
}

// Config is a struct that represents the customizations for a USB gadget.
Expand Down
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ func Main() {

// initialize usb gadget
initUsbGadget()

StartAudioServer()
if _, err := runAudioClient(); err != nil {
logger.Warn().Err(err).Msg("failed to run audio client")
}

if err := setInitialVirtualMediaState(); err != nil {
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
}
Expand Down
30 changes: 28 additions & 2 deletions native.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"time"

"github.com/jetkvm/kvm/resource"

"github.com/pion/webrtc/v4/pkg/media"
)

Expand Down Expand Up @@ -106,6 +105,7 @@ func WriteCtrlMessage(message []byte) error {

var nativeCtrlSocketListener net.Listener //nolint:unused
var nativeVideoSocketListener net.Listener //nolint:unused
var nativeAudioSocketListener net.Listener //nolint:unused

var ctrlClientConnected = make(chan struct{})

Expand Down Expand Up @@ -231,7 +231,7 @@ func handleVideoClient(conn net.Conn) {

scopedLogger.Info().Msg("native video socket client connected")

inboundPacket := make([]byte, maxFrameSize)
inboundPacket := make([]byte, maxVideoFrameSize)
lastFrame := time.Now()
for {
n, err := conn.Read(inboundPacket)
Expand All @@ -251,6 +251,32 @@ func handleVideoClient(conn net.Conn) {
}
}

func handleAudioClient(conn net.Conn) {
defer conn.Close()
scopedLogger := nativeLogger.With().
Str("type", "audio").
Logger()

scopedLogger.Info().Msg("native audio socket client connected")
inboundPacket := make([]byte, maxAudioFrameSize)
for {
n, err := conn.Read(inboundPacket)
if err != nil {
scopedLogger.Warn().Err(err).Msg("error during read")
return
}

if currentSession != nil {
if err := currentSession.AudioTrack.WriteSample(media.Sample{
Data: inboundPacket[:n],
Duration: 20 * time.Millisecond,
}); err != nil {
scopedLogger.Warn().Err(err).Msg("error writing sample")
}
}
}
}

func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
nativeCmdLock.Lock()
defer nativeCmdLock.Unlock()
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/WebRTCVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ export default function WebRTCVideo() {
controls={false}
onPlaying={onVideoPlaying}
onPlay={onVideoPlaying}
muted={true}
muted={false}
playsInline
disablePictureInPicture
controlsList="nofullscreen"
Expand Down
2 changes: 2 additions & 0 deletions ui/src/routes/devices.$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ export default function KvmIdRoute() {
};

setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
// Add audio transceiver to receive audio from the server
pc.addTransceiver("audio", { direction: "recvonly" });

const rpcDataChannel = pc.createDataChannel("rpc");
rpcDataChannel.onopen = () => {
Expand Down
3 changes: 2 additions & 1 deletion video.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
)

// max frame size for 1080p video, specified in mpp venc setting
const maxFrameSize = 1920 * 1080 / 2
const maxVideoFrameSize = 1920 * 1080 / 2
const maxAudioFrameSize = 1500

func writeCtrlAction(action string) error {
actionMessage := map[string]string{
Expand Down
33 changes: 24 additions & 9 deletions webrtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
type Session struct {
peerConnection *webrtc.PeerConnection
VideoTrack *webrtc.TrackLocalStaticSample
AudioTrack *webrtc.TrackLocalStaticSample
ControlChannel *webrtc.DataChannel
RPCChannel *webrtc.DataChannel
HidChannel *webrtc.DataChannel
Expand Down Expand Up @@ -136,22 +137,27 @@ func newSession(config SessionConfig) (*Session, error) {
return nil, err
}

rtpSender, err := peerConnection.AddTrack(session.VideoTrack)
session.AudioTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "kvm")
if err != nil {
return nil, err
}

videoRtpSender, err := peerConnection.AddTrack(session.VideoTrack)
if err != nil {
return nil, err
}

audioRtpSender, err := peerConnection.AddTrack(session.AudioTrack)
if err != nil {
return nil, err
}

// Read incoming RTCP packets
// Before these packets are returned they are processed by interceptors. For things
// like NACK this needs to be called.
go func() {
rtcpBuf := make([]byte, 1500)
for {
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
return
}
}
}()
go drainRtpSender(videoRtpSender)
go drainRtpSender(audioRtpSender)

var isConnected bool

peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
Expand Down Expand Up @@ -203,6 +209,15 @@ func newSession(config SessionConfig) (*Session, error) {
return session, nil
}

func drainRtpSender(rtpSender *webrtc.RTPSender) {
rtcpBuf := make([]byte, 1500)
for {
if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
return
}
}
}

var actionSessions = 0

func onActiveSessionsChanged() {
Expand Down