Skip to content

Commit cc83e41

Browse files
committed
feat: add audio encoder
1 parent 466271d commit cc83e41

File tree

7 files changed

+137
-12
lines changed

7 files changed

+137
-12
lines changed

audio.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package kvm
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os/exec"
7+
"sync"
8+
"syscall"
9+
"time"
10+
)
11+
12+
func startFFmpeg() (cmd *exec.Cmd, err error) {
13+
binaryPath := "/userdata/jetkvm/bin/ffmpeg"
14+
// Run the binary in the background
15+
cmd = exec.Command(binaryPath,
16+
"-f", "alsa",
17+
"-channels", "2",
18+
"-sample_rate", "48000",
19+
"-i", "hw:1,0",
20+
"-c:a", "libopus",
21+
"-b:a", "64k", // ought to be enough for anybody
22+
"-vbr", "off",
23+
"-frame_duration", "20",
24+
"-compression_level", "2",
25+
"-f", "rtp",
26+
"rtp://127.0.0.1:3333")
27+
28+
nativeOutputLock := sync.Mutex{}
29+
nativeStdout := &nativeOutput{
30+
mu: &nativeOutputLock,
31+
logger: nativeLogger.Info().Str("pipe", "stdout"),
32+
}
33+
nativeStderr := &nativeOutput{
34+
mu: &nativeOutputLock,
35+
logger: nativeLogger.Info().Str("pipe", "stderr"),
36+
}
37+
38+
// Redirect stdout and stderr to the current process
39+
cmd.Stdout = nativeStdout
40+
cmd.Stderr = nativeStderr
41+
42+
// Set the process group ID so we can kill the process and its children when this process exits
43+
cmd.SysProcAttr = &syscall.SysProcAttr{
44+
Setpgid: true,
45+
Pdeathsig: syscall.SIGKILL,
46+
}
47+
48+
// Start the command
49+
if err := cmd.Start(); err != nil {
50+
return nil, fmt.Errorf("failed to start binary: %w", err)
51+
}
52+
53+
return
54+
}
55+
56+
func StartNtpAudioServer(handleClient func(net.Conn)) {
57+
scopedLogger := nativeLogger.With().
58+
Logger()
59+
60+
listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 3333})
61+
if err != nil {
62+
scopedLogger.Warn().Err(err).Msg("failed to start server")
63+
return
64+
}
65+
66+
scopedLogger.Info().Msg("server listening")
67+
68+
go func() {
69+
for {
70+
cmd, err := startFFmpeg()
71+
if err != nil {
72+
scopedLogger.Error().Err(err).Msg("failed to start ffmpeg")
73+
}
74+
err = cmd.Wait()
75+
scopedLogger.Error().Err(err).Msg("ffmpeg exited, restarting")
76+
time.Sleep(2 * time.Second)
77+
}
78+
}()
79+
80+
go handleClient(listener)
81+
}

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func Main() {
7676
}()
7777

7878
initUsbGadget()
79+
StartNtpAudioServer(handleAudioClient)
7980

8081
if err := setInitialVirtualMediaState(); err != nil {
8182
logger.Warn().Err(err).Msg("failed to set initial virtual media state")

native.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ func handleVideoClient(conn net.Conn) {
215215

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

218-
inboundPacket := make([]byte, maxFrameSize)
218+
inboundPacket := make([]byte, maxVideoFrameSize)
219219
lastFrame := time.Now()
220220
for {
221221
n, err := conn.Read(inboundPacket)
@@ -235,6 +235,31 @@ func handleVideoClient(conn net.Conn) {
235235
}
236236
}
237237

238+
func handleAudioClient(conn net.Conn) {
239+
defer conn.Close()
240+
scopedLogger := nativeLogger.With().
241+
Str("type", "audio").
242+
Logger()
243+
244+
scopedLogger.Info().Msg("native audio socket client connected")
245+
inboundPacket := make([]byte, maxAudioFrameSize)
246+
for {
247+
n, err := conn.Read(inboundPacket)
248+
if err != nil {
249+
scopedLogger.Warn().Err(err).Msg("error during read")
250+
return
251+
}
252+
253+
logger.Info().Msgf("audio socket msg: %d", n)
254+
255+
if currentSession != nil {
256+
if _, err := currentSession.AudioTrack.Write(inboundPacket[:n]); err != nil {
257+
scopedLogger.Warn().Err(err).Msg("error writing sample")
258+
}
259+
}
260+
}
261+
}
262+
238263
func ExtractAndRunNativeBin() error {
239264
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
240265
if err := ensureBinaryUpdated(binaryPath); err != nil {

ui/src/components/WebRTCVideo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ export default function WebRTCVideo() {
711711
controls={false}
712712
onPlaying={onVideoPlaying}
713713
onPlay={onVideoPlaying}
714-
muted={true}
714+
muted={false}
715715
playsInline
716716
disablePictureInPicture
717717
controlsList="nofullscreen"

ui/src/routes/devices.$id.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ export default function KvmIdRoute() {
480480
};
481481

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

484486
const rpcDataChannel = pc.createDataChannel("rpc");
485487
rpcDataChannel.onopen = () => {

video.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import (
55
)
66

77
// max frame size for 1080p video, specified in mpp venc setting
8-
const maxFrameSize = 1920 * 1080 / 2
8+
const maxVideoFrameSize = 1920 * 1080 / 2
9+
const maxAudioFrameSize = 1500
910

1011
func writeCtrlAction(action string) error {
1112
actionMessage := map[string]string{

webrtc.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
type Session struct {
1919
peerConnection *webrtc.PeerConnection
2020
VideoTrack *webrtc.TrackLocalStaticSample
21+
AudioTrack *webrtc.TrackLocalStaticRTP
2122
ControlChannel *webrtc.DataChannel
2223
RPCChannel *webrtc.DataChannel
2324
HidChannel *webrtc.DataChannel
@@ -136,22 +137,27 @@ func newSession(config SessionConfig) (*Session, error) {
136137
return nil, err
137138
}
138139

139-
rtpSender, err := peerConnection.AddTrack(session.VideoTrack)
140+
session.AudioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "kvm")
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
videoRtpSender, err := peerConnection.AddTrack(session.VideoTrack)
146+
if err != nil {
147+
return nil, err
148+
}
149+
150+
audioRtpSender, err := peerConnection.AddTrack(session.AudioTrack)
140151
if err != nil {
141152
return nil, err
142153
}
143154

144155
// Read incoming RTCP packets
145156
// Before these packets are returned they are processed by interceptors. For things
146157
// like NACK this needs to be called.
147-
go func() {
148-
rtcpBuf := make([]byte, 1500)
149-
for {
150-
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
151-
return
152-
}
153-
}
154-
}()
158+
go drainRtpSender(videoRtpSender)
159+
go drainRtpSender(audioRtpSender)
160+
155161
var isConnected bool
156162

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

212+
func drainRtpSender(rtpSender *webrtc.RTPSender) {
213+
rtcpBuf := make([]byte, 1500)
214+
for {
215+
if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
216+
return
217+
}
218+
}
219+
}
220+
206221
var actionSessions = 0
207222

208223
func onActiveSessionsChanged() {

0 commit comments

Comments
 (0)