Compare commits

...

6 Commits

Author SHA1 Message Date
Qishuai Liu 1efa74ba72
Merge 28a8fa05cc into 960f555790 2025-07-01 08:23:33 +10:00
Qishuai Liu 28a8fa05cc
feat: use native jetkvm-audio 2025-06-26 00:30:00 +09:00
Qishuai Liu c529c903d0
Merge remote-tracking branch 'upstream/dev' into feat/usb-audio 2025-06-22 00:36:01 +09:00
Qishuai Liu 9d12dd1e54
fix: audio rtp timestamp 2025-05-16 23:11:22 +09:00
Qishuai Liu cc83e4193f
feat: add audio encoder 2025-05-14 23:41:48 +09:00
Qishuai Liu 466271d935
feat: add usb gadget audio config 2025-05-14 23:15:45 +09:00
10 changed files with 98 additions and 13 deletions

14
audio.go Normal file
View File

@ -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")
}

View File

@ -128,6 +128,7 @@ var defaultConfig = &Config{
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
Audio: true,
},
NetworkConfig: &network.NetworkConfig{},
DefaultLogLevel: "INFO",

View File

@ -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 {
@ -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
}

View File

@ -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.

View File

@ -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")
}

View File

@ -13,7 +13,6 @@ import (
"time"
"github.com/jetkvm/kvm/resource"
"github.com/pion/webrtc/v4/pkg/media"
)
@ -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{})
@ -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)
@ -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()

View File

@ -695,7 +695,7 @@ export default function WebRTCVideo() {
controls={false}
onPlaying={onVideoPlaying}
onPlay={onVideoPlaying}
muted={true}
muted={false}
playsInline
disablePictureInPicture
controlsList="nofullscreen"

View File

@ -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 = () => {

View File

@ -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{

View File

@ -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
@ -136,7 +137,17 @@ 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
}
@ -144,14 +155,9 @@ func newSession(config SessionConfig) (*Session, error) {
// 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) {
@ -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() {