mirror of https://github.com/jetkvm/kvm.git
169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package kvm
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"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
|
|
}
|
|
|
|
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() (*Session, error) {
|
|
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
|
ICEServers: []webrtc.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)
|
|
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")
|
|
}
|