kvm/webrtc.go

198 lines
5.1 KiB
Go

package kvm
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"strings"
"github.com/pion/webrtc/v4"
)
type Session struct {
peerConnection *webrtc.PeerConnection
VideoTrack *webrtc.TrackLocalStaticSample
ControlChannel *webrtc.DataChannel
RPCChannel *webrtc.DataChannel
HidChannel *webrtc.DataChannel
DiskChannel *webrtc.DataChannel
shouldUmountVirtualMedia bool
}
type SessionConfig struct {
ICEServers []string
LocalIP string
IsCloud bool
}
func (s *Session) ExchangeOffer(offerStr string) (string, error) {
b, err := base64.StdEncoding.DecodeString(offerStr)
if err != nil {
return "", err
}
offer := webrtc.SessionDescription{}
err = json.Unmarshal(b, &offer)
if err != nil {
return "", err
}
// Set the remote SessionDescription
if err = s.peerConnection.SetRemoteDescription(offer); err != nil {
return "", err
}
// Create answer
answer, err := s.peerConnection.CreateAnswer(nil)
if err != nil {
return "", err
}
// Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc.GatheringCompletePromise(s.peerConnection)
// Sets the LocalDescription, and starts our UDP listeners
if err = s.peerConnection.SetLocalDescription(answer); err != nil {
return "", 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
localDescription, err := json.Marshal(s.peerConnection.LocalDescription())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(localDescription), nil
}
func newSession(config SessionConfig) (*Session, error) {
webrtcSettingEngine := webrtc.SettingEngine{}
iceServer := webrtc.ICEServer{}
if config.IsCloud {
if config.ICEServers == nil {
fmt.Printf("ICE Servers not provided by cloud")
} else {
iceServer.URLs = config.ICEServers
fmt.Printf("Using ICE Servers provided by cloud: %v\n", iceServer.URLs)
}
if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil {
fmt.Printf("Local IP address %v not provided or invalid, won't set NAT1To1IPs\n", config.LocalIP)
} else {
webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx)
fmt.Printf("Setting NAT1To1IPs to %s\n", config.LocalIP)
}
}
api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{iceServer},
})
if err != nil {
return nil, err
}
session := &Session{peerConnection: peerConnection}
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())
switch d.Label() {
case "rpc":
session.RPCChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) {
go onRPCMessage(msg, session)
})
triggerOTAStateUpdate()
triggerVideoStateUpdate()
triggerUSBStateUpdate()
case "disk":
session.DiskChannel = d
d.OnMessage(onDiskMessage)
case "terminal":
handleTerminalChannel(d)
case "serial":
handleSerialChannel(d)
default:
if strings.HasPrefix(d.Label(), uploadIdPrefix) {
go handleUploadChannel(d)
}
}
})
session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm")
if err != nil {
return nil, err
}
rtpSender, err := peerConnection.AddTrack(session.VideoTrack)
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
}
}
}()
var isConnected bool
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
if connectionState == webrtc.ICEConnectionStateConnected {
if !isConnected {
isConnected = true
actionSessions++
onActiveSessionsChanged()
if actionSessions == 1 {
onFirstSessionConnected()
}
}
}
//state changes on closing browser tab disconnected->failed, we need to manually close it
if connectionState == webrtc.ICEConnectionStateFailed {
_ = peerConnection.Close()
}
if connectionState == webrtc.ICEConnectionStateClosed {
if session == currentSession {
currentSession = nil
}
if session.shouldUmountVirtualMedia {
err := rpcUnmountImage()
logger.Debugf("unmount image failed on connection close %v", err)
}
if isConnected {
isConnected = false
actionSessions--
onActiveSessionsChanged()
if actionSessions == 0 {
onLastSessionDisconnected()
}
}
}
})
return session, nil
}
var actionSessions = 0
func onActiveSessionsChanged() {
requestDisplayUpdate()
}
func onFirstSessionConnected() {
_ = writeCtrlAction("start_video")
}
func onLastSessionDisconnected() {
_ = writeCtrlAction("stop_video")
}